Compare commits

...

84 Commits

Author SHA1 Message Date
Erik 668262aa1a Improve comment for helpers.entity.entity_sources 2025-06-11 12:42:21 +02:00
Erik Montnemery c4c8f88765 Simplify helper_integration.async_handle_source_entity_changes (#146516) 2025-06-11 12:27:51 +02:00
epenet f908e0cf4d Bump pybotvac to 0.0.28 (#146513) 2025-06-11 12:19:54 +02:00
epenet 29c720a66d Bump weheat to 2025.6.10 (#146515) 2025-06-11 12:19:06 +02:00
epenet 4e628dbd9f Bump sensorpush-api to 2.1.3 (#146514) 2025-06-11 12:18:55 +02:00
Petro31 37d904dfdc Add color_temp_kelvin to set_temperature action variables (#146448) 2025-06-11 11:58:07 +02:00
Åke Strandberg a53997dfc7 Graceful handling of missing datapoint in myuplink (#146517) 2025-06-11 11:55:28 +02:00
Joost Lekkerkerker dd216ac15b Split deprecated system issue in 2 places (#146453) 2025-06-11 11:35:14 +02:00
Erik Montnemery 2afdec4711 Do not remove derivative config entry when input sensor is removed (#146506)
* Do not remove derivative config entry when input sensor is removed

* Add comments

* Update homeassistant/helpers/helper_integration.py

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

---------

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-11 11:19:44 +02:00
karwosts 5b4c309170 Create a deprecation/repair for sensor.sun_solar_rising (#146462)
* Create a deprecation/repair for `sensor.sun_solar_rising`

* test

* Update homeassistant/components/sun/strings.json
2025-06-11 11:02:14 +02:00
hanwg 8deec55204 Add service validation for send file for Telegram bot integration (#146192)
* added service validation for send file

* update strings

* Apply suggestions from code review

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

* updated exception in tests

* removed TypeError since it is not thrown

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-11 10:59:08 +02:00
Robert Resch f0a2c4e30a Bump deebot-client to 13.3.0 (#146507) 2025-06-11 10:49:38 +02:00
Joost Lekkerkerker e9a71a8d7f Explain Nest setup (#146217) 2025-06-11 10:31:08 +02:00
Felix Schneider 1462366764 Bump apsystems to 2.7.0 (#146485) 2025-06-11 10:26:01 +02:00
Artur Pragacz 33528eb6bd Update pywizlight to 0.6.3 (#146490) 2025-06-11 08:26:55 +02:00
epenet 776a014ab0 Drop deprecated add_event service in google (#146432) 2025-06-10 20:35:17 -07:00
Michael Hansen ea202eff66 Bump intents to 2025.6.10 (#146491) 2025-06-10 18:16:18 -05:00
Tsvi Mostovicz b7404f5a05 Fix Jewish calendar not updating (#146465) 2025-06-10 21:25:47 +02:00
Joost Lekkerkerker d015dff855 Remove DHCP discovery from Amazon Devices (#146476) 2025-06-10 20:55:00 +02:00
Joost Lekkerkerker 2f1977fa0c Fix typo in hassio (#146474) 2025-06-10 20:52:43 +02:00
Erik Montnemery 26fe23eb5c Improve support for trigger platforms with multiple triggers (#144827)
* Improve support for trigger platforms with multiple triggers

* Adjust zwave_js

* Refactor the Trigger class

* Silence mypy

* Adjust

* Revert "Adjust"

This reverts commit 17b3d16a26.

* Revert "Silence mypy"

This reverts commit c2a011b16f.

* Reapply "Adjust"

This reverts commit c64ba202dd19da9de08c504f8163ec51acbebab0.

* Apply suggestions from code review

* Revert "Apply suggestions from code review"

This reverts commit 0314955c5a15548b8a4ce69aab7b25452fe4b1e0.
2025-06-10 20:48:51 +02:00
hahn-th dbfecf99dc Bump homematicip to 2.0.4 (#144096)
* Bump to 2.0.2 with all necessary changes

* bump to prerelease

* add addiional tests

* Bump to homematicip 2.0.3

* do not delete device

* Setup BRAND_SWITCH_MEASURING as light

* bump to 2.0.4

* refactor test_remove_obsolete_entities

* move test

* use const from homematicip lib
2025-06-10 20:44:06 +02:00
hanwg 4d28992f2b Add Telegram bot webhooks tests (#146436)
* add tests for webhooks

* added asserts
2025-06-10 19:58:15 +02:00
Markus Adrario 7a428a66bd Add support for HeatIt Thermostat TF056 to homee (#145515)
* adapt climate for Heatit TF 056

* add sensors & numbers for Heatit TF056

* Add select for Heatit TF056

* Adapt climat tests for changes

* Fix sentence case

* fix review comments

* Update homeassistant/components/homee/climate.py

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

* fix tests

* update diagnostics snapshot for this change

---------

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-06-10 19:41:13 +02:00
G Johansson 481bf2694b Fix incorrect categories handling in holiday (#146470) 2025-06-10 19:28:48 +02:00
Simone Chemelli 5cc9cc3c99 Fix EntityCategory for binary_sensor platform in Amazon Devices (#146472)
* Fix EntityCategory for  binary_sensor platform in Amazon Devices

* update snapshots
2025-06-10 19:28:37 +02:00
Whitney Young 87ce683b39 Add tests for initial state of OpenUV sensors (#146464)
This is a followup to #146408 to add test coverage.
2025-06-10 19:28:29 +02:00
Simone Chemelli 936d56f9af Avoid closing shared aiohttp session in Vodafone Station (#146471) 2025-06-10 19:18:19 +02:00
starkillerOG d71ddcf69e Reolink conserve battery (#145452) 2025-06-10 18:05:55 +02:00
Robert Resch 3af2746fea Update wording deprecated system package integration repair (#146450)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-10 18:04:22 +02:00
Joost Lekkerkerker 5b6d7142fb Bump pySmartThings to 3.2.4 (#146459) 2025-06-10 17:37:21 +02:00
Whitney Young 7aa9301038 Fix initial state of UV protection window (#146408)
The `binary_sensor` is created when the config entry is loaded after the
`async_config_entry_first_refresh` has completed (during the forward of
setup to platforms). Therefore, the update coordinator will already have
data and will not trigger the invocation of
`_handle_coordinator_update`.

Fixing this just means performing the same update at initialization.
2025-06-10 17:35:40 +02:00
hanwg 627831dfaf Fix Telegram bot leave_chat service action (#146139)
* bug fix for leave chat

* update strings
2025-06-10 17:33:54 +02:00
Joost Lekkerkerker db8a6f8583 Catch exception before retrying in AirGradient (#146460) 2025-06-10 17:31:30 +02:00
Paulus Schoutsen 014010acbd Assist Pipeline: Intent progress event when we start streaming (#146388)
Intent progress event when we start streaming
2025-06-10 09:55:43 -05:00
Arie Catsman 9b90ed04e5 fix possible mac collision in enphase_envoy (#145549)
* fix possible mac collision in enphase_envoy

* remove redundant device registry async_get
2025-06-10 16:25:26 +02:00
hanwg 0f27d0bf4a Bug fix for Telegram bot integration: fix async_unload_entry error for polling bot (#146277)
* removed reload from update_listener

* removed reload from update_listener
2025-06-10 16:24:51 +02:00
Andrea Turri 1fa55f96f8 Add evaporate water program id for Miele oven (#145996) 2025-06-10 16:23:55 +02:00
Jamin 2d60115ec6 Check hangup error in voip (#146423)
Check hangup error

Prevent an error where the call end future may have already been set
when a hangup is detected.
2025-06-10 16:22:53 +02:00
Luca Schröder 3b81480091 Update caldav to 1.6.0 (#146456)
Fixes #140798
2025-06-10 16:20:35 +02:00
Will Schlitzer 255acfa8c0 Fix typo in overseerr component docstring (#146457)
Change 'airgradient' to 'overseerr' in sensor.py
2025-06-10 16:15:40 +02:00
Marc Mueller 4617cc4e0a Update awesomeversion to 25.5.0 (#146032) 2025-06-10 15:44:53 +02:00
tronikos b9e8cfb291 Handle grpc errors in Google Assistant SDK (#146438) 2025-06-10 15:31:32 +02:00
J. Nick Koston 7da1671b06 Shift ESPHome log parsing to the library (#146349) 2025-06-10 15:30:19 +02:00
Marc Mueller 6c5f7eabff Fix RuntimeWarning in rest tests (#146452) 2025-06-10 15:26:07 +02:00
Ian f448f488ba Throttle Nextbus if we are reaching the rate limit (#146064)
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
2025-06-10 15:03:20 +02:00
Marc Mueller 20b5d5a755 Add requests to hassfest requirements check (#146446) 2025-06-10 15:01:05 +02:00
Marc Mueller bb38a3a8ac Update requests to 2.32.4 (#146445) 2025-06-10 15:00:41 +02:00
Brett Adams d0d1fb2da7 Prevent energy history returning zero in Teslemetry (#146202) 2025-06-10 15:00:02 +02:00
Marc Mueller d82be09ed4 Update aiomealie to 0.9.6 (#146447) 2025-06-10 14:53:56 +02:00
Joost Lekkerkerker 110627e16e Return expected state in SmartThings water heater (#146449) 2025-06-10 14:52:24 +02:00
Klaas Schoute b77ef7304a Change interval for Powerfox integration (#146348) 2025-06-10 14:38:52 +02:00
Erik Montnemery 16a0b7f44e Handle changes to source entity in derivative helper (#146407)
* Handle changes to source entity in derivative helper

* Rename helper function, improve docstring

* Add tests

* Improve derivative tests

* Deduplicate tests

* Rename helpers/helper_entity.py to helpers/helper_integration.py

* Rename tests
2025-06-10 14:31:18 +02:00
Joost Lekkerkerker 4fdbb9c0e2 Remove __all__ from switch_as_x (#146331)
* Remove `__all__` from switch_as_x

* Update homeassistant/components/switch_as_x/__init__.py
2025-06-10 14:21:01 +02:00
J. Diego Rodríguez Royo c32a988838 Improvements for Home Connect application credentials string (#146443) 2025-06-10 14:11:07 +02:00
Jan-Philipp Benecke 927c9d3480 Improve error logging in trend binary sensor (#146358) 2025-06-10 14:10:49 +02:00
Joost Lekkerkerker bf776d33b2 Explain Withings setup (#146216) 2025-06-10 14:10:35 +02:00
epenet 279539265b Use async_load_fixture in modern_forms tests (#146011) 2025-06-10 12:38:25 +02:00
J. Diego Rodríguez Royo 4acad77437 Fix typo at application credentials string at Home Connect integration (#146442)
Fix typos
2025-06-10 11:56:24 +02:00
J. Nick Koston 0c5b7401b9 Use entity unique id for ESPHome media player formats (#146318) 2025-06-10 11:48:11 +02:00
Erik Montnemery ce739fd9b6 Restore entity ID and user customizations of deleted entities (#145278)
* Restore entity ID and user customizations of deleted entities

* Clear removed areas, categories and labels from deleted entities

* Correct test

* Fix logic for disabled_by and hidden_by

* Improve test coverage

* Fix sorting

* Always restore disabled_by and hidden_by

* Update mqtt test

* Update pglab tests
2025-06-10 11:47:54 +02:00
Erik Montnemery 11d9014be0 Restore user customizations of deleted devices (#145191)
* Restore user customizations of deleted devices

* Apply suggestions from code review

* Improve test coverage

* Always restore disabled_by
2025-06-10 11:47:39 +02:00
J. Nick Koston c9dcb1c11b Bump propcache to 0.3.2 (#146418) 2025-06-10 11:44:34 +02:00
J. Diego Rodríguez Royo ef7f32a28d Explain Home Connect setup (#146356)
* Explain Home Connect setup

* Avoid using "we"

* Fix login spelling

* Fix signup spelling
2025-06-10 11:41:36 +02:00
J. Nick Koston 4f5cf5797f Bump yarl to 1.20.1 (#146424) 2025-06-10 11:26:29 +02:00
Retha Runolfsson 4c5485ad04 Bump pyswitchbot to 0.66.0 (#146430)
bump pyswitchbot to 0.66.0
2025-06-10 11:16:08 +02:00
Franck Nijhof 5ad96dedfa Reformat Dockerfile to reduce merge conflicts (#146435) 2025-06-10 11:14:31 +02:00
epenet 0c18fe35e5 Migrate cloudflare to use runtime data (#146429) 2025-06-10 09:50:31 +02:00
epenet 6a23ad96ca Move google assistant sdk services to separate module (#146434) 2025-06-10 00:49:56 -07:00
J. Nick Koston def0384608 Bump aiohttp to 3.12.12 (#146426) 2025-06-10 09:39:53 +02:00
Raphael Hehl a4d12694da Bump uiprotect to 7.13.0 (#146410) 2025-06-09 19:26:54 -05:00
J. Nick Koston 2278e3f06f Bump aioesphomeapi to 32.2.1 (#146375) 2025-06-09 19:25:29 -05:00
Will Schlitzer 0144a0bb1f Fix minor docstring typos in jellyfin component media_source.py (#146398) 2025-06-09 20:12:32 +02:00
Imeon-Energy 7cc8f91bf9 Basic entity class for Imeon inverter integration (#145778)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-06-09 20:04:25 +02:00
hanwg d58157ca9e Bug fix for Telegram bot integration: handle last message id (#146378) 2025-06-09 20:01:16 +02:00
David Knowles f401ffb08c Bump pydrawise to 2025.6.0 (#146369) 2025-06-09 20:00:37 +02:00
Simone Chemelli 8f7b831b94 Bump aioamazondevices to 3.0.6 (#146385) 2025-06-09 19:59:02 +02:00
wittypluck 9ed6b591a5 Fix CO concentration unit in OpenWeatherMap (#146403) 2025-06-09 19:55:09 +02:00
Michael Davie 98ea067285 Bump env-canada to v0.11.2 (#146371) 2025-06-09 12:53:44 -05:00
G Johansson 7e507dd378 Bump pynordpool to 0.3.0 (#146396) 2025-06-09 19:51:46 +02:00
Erik Montnemery 8e87223c40 Update switch_as_x to handle wrapped switch moved to another device (#146387)
* Update switch_as_x to handle wrapped switch moved to another device

* Reload switch_as_x config entry after updating device

* Make sure the switch_as_x entity is not removed
2025-06-09 17:04:55 +02:00
Abílio Costa 0cce4d1b81 Test all device classes in Sensor device condition/trigger tests (#146366) 2025-06-09 14:22:58 +01:00
Erik Montnemery 46dcc91510 Fix switch_as_x entity_id tracking (#146386) 2025-06-09 13:24:40 +02:00
Markus Adrario b1a2af9fd3 Add Homee diagnostics platform (#146340)
* Initial dignostics implementation

* Add diagnostics tests

* change data-set for device diagnostics

* adapt for upcoming pyHomee release

* other solution

* fix review and more
2025-06-09 13:24:07 +02:00
173 changed files with 7398 additions and 1707 deletions
@@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self._current_version = (
await self.client.get_current_measures()
).firmware_version
try:
self._current_version = (
await self.client.get_current_measures()
).firmware_version
except AirGradientError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": str(error)},
) from error
async def _async_update_data(self) -> AirGradientData:
try:
@@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -34,10 +35,12 @@ BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda _device: _device.online,
),
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda _device: _device.bluetooth_state,
),
@@ -3,120 +3,10 @@
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "007147*" },
{ "macaddress": "00FC8B*" },
{ "macaddress": "0812A5*" },
{ "macaddress": "086AE5*" },
{ "macaddress": "08849D*" },
{ "macaddress": "089115*" },
{ "macaddress": "08A6BC*" },
{ "macaddress": "08C224*" },
{ "macaddress": "0CDC91*" },
{ "macaddress": "0CEE99*" },
{ "macaddress": "1009F9*" },
{ "macaddress": "109693*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "10CE02*" },
{ "macaddress": "140AC5*" },
{ "macaddress": "149138*" },
{ "macaddress": "1848BE*" },
{ "macaddress": "1C12B0*" },
{ "macaddress": "1C4D66*" },
{ "macaddress": "1C93C4*" },
{ "macaddress": "1CFE2B*" },
{ "macaddress": "244CE3*" },
{ "macaddress": "24CE33*" },
{ "macaddress": "2873F6*" },
{ "macaddress": "2C71FF*" },
{ "macaddress": "34AFB3*" },
{ "macaddress": "34D270*" },
{ "macaddress": "38F73D*" },
{ "macaddress": "3C5CC4*" },
{ "macaddress": "3CE441*" },
{ "macaddress": "440049*" },
{ "macaddress": "40A2DB*" },
{ "macaddress": "40A9CF*" },
{ "macaddress": "40B4CD*" },
{ "macaddress": "443D54*" },
{ "macaddress": "44650D*" },
{ "macaddress": "485F2D*" },
{ "macaddress": "48785E*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "4CEFC0*" },
{ "macaddress": "5007C3*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "50F5DA*" },
{ "macaddress": "5C415A*" },
{ "macaddress": "6837E9*" },
{ "macaddress": "6854FD*" },
{ "macaddress": "689A87*" },
{ "macaddress": "68B691*" },
{ "macaddress": "68DBF5*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "6C0C9A*" },
{ "macaddress": "6C5697*" },
{ "macaddress": "7458F3*" },
{ "macaddress": "74C246*" },
{ "macaddress": "74D637*" },
{ "macaddress": "74E20C*" },
{ "macaddress": "74ECB2*" },
{ "macaddress": "786C84*" },
{ "macaddress": "78A03F*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "7C6305*" },
{ "macaddress": "7CD566*" },
{ "macaddress": "8871E5*" },
{ "macaddress": "901195*" },
{ "macaddress": "90235B*" },
{ "macaddress": "90A822*" },
{ "macaddress": "90F82E*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "98CCF3*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A002DC*" },
{ "macaddress": "A0D2B1*" },
{ "macaddress": "A40801*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "AC416A*" },
{ "macaddress": "AC63BE*" },
{ "macaddress": "ACCCFC*" },
{ "macaddress": "B0739C*" },
{ "macaddress": "B0CFCB*" },
{ "macaddress": "B0F7C4*" },
{ "macaddress": "B85F98*" },
{ "macaddress": "C091B9*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "C49500*" },
{ "macaddress": "C86C3D*" },
{ "macaddress": "CC9EA2*" },
{ "macaddress": "CCF735*" },
{ "macaddress": "DC54D7*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "D8FBD6*" },
{ "macaddress": "DC91BF*" },
{ "macaddress": "DCA0D0*" },
{ "macaddress": "E0F728*" },
{ "macaddress": "EC2BEB*" },
{ "macaddress": "EC8AC4*" },
{ "macaddress": "ECA138*" },
{ "macaddress": "F02F9E*" },
{ "macaddress": "F0272D*" },
{ "macaddress": "F0F0A4*" },
{ "macaddress": "F4032A*" },
{ "macaddress": "F854B8*" },
{ "macaddress": "FC492D*" },
{ "macaddress": "FC65DE*" },
{ "macaddress": "FCA183*" },
{ "macaddress": "FCE9D8*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.0.5"]
"requirements": ["aioamazondevices==3.0.6"]
}
@@ -45,7 +45,9 @@ rules:
discovery-update-info:
status: exempt
comment: Network information not relevant
discovery: done
discovery:
status: exempt
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.6.0"]
"requirements": ["apsystems-ez1==2.7.0"]
}
@@ -1207,6 +1207,15 @@ class PipelineRun:
self._streamed_response_text = True
self.process_event(
PipelineEvent(
PipelineEventType.INTENT_PROGRESS,
{
"tts_start_streaming": True,
},
)
)
async def tts_input_stream_generator() -> AsyncGenerator[str]:
"""Yield TTS input stream."""
while (tts_input := await tts_input_stream.get()) is not None:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
}
+24 -13
View File
@@ -3,7 +3,8 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
import socket
@@ -26,8 +27,18 @@ from .const import CONF_RECORDS, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_UPDATE
_LOGGER = logging.getLogger(__name__)
type CloudflareConfigEntry = ConfigEntry[CloudflareRuntimeData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class CloudflareRuntimeData:
"""Runtime data for Cloudflare config entry."""
client: pycfdns.Client
dns_zone: pycfdns.ZoneModel
async def async_setup_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
"""Set up Cloudflare from a config entry."""
session = async_get_clientsession(hass)
client = pycfdns.Client(
@@ -45,12 +56,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except pycfdns.ComunicationException as error:
raise ConfigEntryNotReady from error
async def update_records(now):
entry.runtime_data = CloudflareRuntimeData(client, dns_zone)
async def update_records(now: datetime) -> None:
"""Set up recurring update."""
try:
await _async_update_cloudflare(
hass, client, dns_zone, entry.data[CONF_RECORDS]
)
await _async_update_cloudflare(hass, entry)
except (
pycfdns.AuthenticationException,
pycfdns.ComunicationException,
@@ -60,9 +71,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_records_service(call: ServiceCall) -> None:
"""Set up service for manual trigger."""
try:
await _async_update_cloudflare(
hass, client, dns_zone, entry.data[CONF_RECORDS]
)
await _async_update_cloudflare(hass, entry)
except (
pycfdns.AuthenticationException,
pycfdns.ComunicationException,
@@ -79,7 +88,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: CloudflareConfigEntry) -> bool:
"""Unload Cloudflare config entry."""
return True
@@ -87,10 +96,12 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_update_cloudflare(
hass: HomeAssistant,
client: pycfdns.Client,
dns_zone: pycfdns.ZoneModel,
target_records: list[str],
entry: CloudflareConfigEntry,
) -> None:
client = entry.runtime_data.client
dns_zone = entry.runtime_data.dns_zone
target_records: list[str] = entry.data[CONF_RECORDS]
_LOGGER.debug("Starting update for zone %s", dns_zone["name"])
records = await client.list_dns_records(zone_id=dns_zone["id"], type="A")
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
}
@@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -17,6 +19,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry.entry_id, entry.options[CONF_SOURCE]
)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SOURCE: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_SOURCE]
),
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
}
@@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
)
return
device_registry.async_update_device(
device_id=envoy_device.id,
new_connections={connection},
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={
(
DOMAIN,
self.envoy_serial_number,
)
},
connections={connection},
)
_LOGGER.debug("added connection: %s to %s", connection, self.name)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.10.2"]
"requirements": ["env-canada==0.11.2"]
}
@@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
_static_info: _InfoT
_state: _StateT
_has_state: bool
unique_id: str
def __init__(
self,
+10 -13
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
import asyncio
from functools import partial
import logging
import re
from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import (
@@ -23,6 +22,7 @@ from aioesphomeapi import (
RequiresEncryptionAPIError,
UserService,
UserServiceArgType,
parse_log_message,
)
from awesomeversion import AwesomeVersion
import voluptuous as vol
@@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = {
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
}
# 7-bit and 8-bit C1 ANSI sequences
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
ANSI_ESCAPE_78BIT = re.compile(
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
)
@callback
@@ -387,13 +382,15 @@ class ESPHomeManager:
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
"""Handle a log message from the API."""
log: bytes = msg.message
_LOGGER.log(
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
"%s: %s",
self.entry.title,
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
)
for line in parse_log_message(
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
):
_LOGGER.log(
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
"%s: %s",
self.entry.title,
line,
)
@callback
def _async_get_equivalent_log_level(self) -> LogLevel:
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==32.2.0",
"aioesphomeapi==32.2.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
],
@@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
if self._static_info.supports_pause:
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
self._attr_supported_features = flags
self._entry_data.media_player_formats[static_info.unique_id] = cast(
self._entry_data.media_player_formats[self.unique_id] = cast(
MediaPlayerInfo, static_info
).supported_formats
@@ -114,9 +114,8 @@ class EsphomeMediaPlayer(
media_id = async_process_play_media_url(self.hass, media_id)
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
supported_formats: list[MediaPlayerSupportedFormat] | None = (
self._entry_data.media_player_formats.get(self._static_info.unique_id)
self._entry_data.media_player_formats.get(self.unique_id)
)
if (
@@ -139,7 +138,7 @@ class EsphomeMediaPlayer(
async def async_will_remove_from_hass(self) -> None:
"""Handle entity being removed."""
await super().async_will_remove_from_hass()
self._entry_data.media_player_formats.pop(self.entity_id, None)
self._entry_data.media_player_formats.pop(self.unique_id, None)
def _get_proxy_url(
self,
+3 -138
View File
@@ -10,7 +10,6 @@ from typing import Any
import aiohttp
from gcal_sync.api import GoogleCalendarService
from gcal_sync.exceptions import ApiException, AuthException
from gcal_sync.model import DateOrDatetime, Event
import voluptuous as vol
import yaml
@@ -21,32 +20,14 @@ from homeassistant.const import (
CONF_OFFSET,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import generate_entity_id
from .api import ApiAuthImpl, get_feature_access
from .const import (
DOMAIN,
EVENT_DESCRIPTION,
EVENT_END_DATE,
EVENT_END_DATETIME,
EVENT_IN,
EVENT_IN_DAYS,
EVENT_IN_WEEKS,
EVENT_LOCATION,
EVENT_START_DATE,
EVENT_START_DATETIME,
EVENT_SUMMARY,
EVENT_TYPES_CONF,
FeatureAccess,
)
from .const import DOMAIN
from .store import GoogleConfigEntry, GoogleRuntimeData, LocalCalendarStore
_LOGGER = logging.getLogger(__name__)
@@ -63,10 +44,6 @@ CONF_MAX_RESULTS = "max_results"
DEFAULT_CONF_OFFSET = "!!"
EVENT_CALENDAR_ID = "calendar_id"
SERVICE_ADD_EVENT = "add_event"
YAML_DEVICES = f"{DOMAIN}_calendars.yaml"
PLATFORMS = [Platform.CALENDAR]
@@ -100,41 +77,6 @@ DEVICE_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_EVENT_IN_TYPES = vol.Schema(
{
vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
}
)
ADD_EVENT_SERVICE_SCHEMA = vol.All(
cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN),
{
vol.Required(EVENT_CALENDAR_ID): cv.string,
vol.Required(EVENT_SUMMARY): cv.string,
vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
vol.Optional(EVENT_LOCATION, default=""): cv.string,
vol.Inclusive(
EVENT_START_DATE, "dates", "Start and end dates must both be specified"
): cv.date,
vol.Inclusive(
EVENT_END_DATE, "dates", "Start and end dates must both be specified"
): cv.date,
vol.Inclusive(
EVENT_START_DATETIME,
"datetimes",
"Start and end datetimes must both be specified",
): cv.datetime,
vol.Inclusive(
EVENT_END_DATETIME,
"datetimes",
"Start and end datetimes must both be specified",
): cv.datetime,
vol.Optional(EVENT_IN): _EVENT_IN_TYPES,
},
)
async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bool:
"""Set up Google from a config entry."""
@@ -190,10 +132,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id)
# Only expose the add event service if we have the correct permissions
if get_feature_access(entry) is FeatureAccess.read_write:
await async_setup_add_event_service(hass, calendar_service)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@@ -225,79 +163,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> N
await store.async_remove()
async def async_setup_add_event_service(
hass: HomeAssistant,
calendar_service: GoogleCalendarService,
) -> None:
"""Add the service to add events."""
async def _add_event(call: ServiceCall) -> None:
"""Add a new event to calendar."""
_LOGGER.warning(
"The Google Calendar add_event service has been deprecated, and "
"will be removed in a future Home Assistant release. Please move "
"calls to the create_event service"
)
start: DateOrDatetime | None = None
end: DateOrDatetime | None = None
if EVENT_IN in call.data:
if EVENT_IN_DAYS in call.data[EVENT_IN]:
now = datetime.now()
start_in = now + timedelta(days=call.data[EVENT_IN][EVENT_IN_DAYS])
end_in = start_in + timedelta(days=1)
start = DateOrDatetime(date=start_in)
end = DateOrDatetime(date=end_in)
elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
now = datetime.now()
start_in = now + timedelta(weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
end_in = start_in + timedelta(days=1)
start = DateOrDatetime(date=start_in)
end = DateOrDatetime(date=end_in)
elif EVENT_START_DATE in call.data and EVENT_END_DATE in call.data:
start = DateOrDatetime(date=call.data[EVENT_START_DATE])
end = DateOrDatetime(date=call.data[EVENT_END_DATE])
elif EVENT_START_DATETIME in call.data and EVENT_END_DATETIME in call.data:
start_dt = call.data[EVENT_START_DATETIME]
end_dt = call.data[EVENT_END_DATETIME]
start = DateOrDatetime(
date_time=start_dt, timezone=str(hass.config.time_zone)
)
end = DateOrDatetime(date_time=end_dt, timezone=str(hass.config.time_zone))
if start is None or end is None:
raise ValueError(
"Missing required fields to set start or end date/datetime"
)
event = Event(
summary=call.data[EVENT_SUMMARY],
description=call.data[EVENT_DESCRIPTION],
start=start,
end=end,
)
if location := call.data.get(EVENT_LOCATION):
event.location = location
try:
await calendar_service.async_create_event(
call.data[EVENT_CALENDAR_ID],
event,
)
except ApiException as err:
raise HomeAssistantError(str(err)) from err
hass.services.async_register(
DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
)
def get_calendar_info(
hass: HomeAssistant, calendar: Mapping[str, Any]
) -> dict[str, Any]:
@@ -2,21 +2,13 @@
from __future__ import annotations
import dataclasses
import aiohttp
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
@@ -31,21 +23,9 @@ from .helpers import (
GoogleAssistantSDKConfigEntry,
GoogleAssistantSDKRuntimeData,
InMemoryStorage,
async_send_text_commands,
best_matching_language_code,
)
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
{
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
),
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
},
)
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -58,6 +38,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
)
async_setup_services(hass)
return True
@@ -81,8 +63,6 @@ async def async_setup_entry(
mem_storage = InMemoryStorage(hass)
hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage))
await async_setup_service(hass)
entry.runtime_data = GoogleAssistantSDKRuntimeData(
session=session, mem_storage=mem_storage
)
@@ -105,36 +85,6 @@ async def async_unload_entry(
return True
async def async_setup_service(hass: HomeAssistant) -> None:
"""Add the services for Google Assistant SDK."""
async def send_text_command(call: ServiceCall) -> ServiceResponse:
"""Send a text command to Google Assistant SDK."""
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
media_players: list[str] | None = call.data.get(
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
)
command_response_list = await async_send_text_commands(
hass, commands, media_players
)
if call.return_response:
return {
"responses": [
dataclasses.asdict(command_response)
for command_response in command_response_list
]
}
return None
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT_COMMAND,
send_text_command,
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent):
"""Google Assistant SDK conversation agent."""
@@ -12,6 +12,7 @@ import aiohttp
from aiohttp import web
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
from grpc import RpcError
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
@@ -25,6 +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.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later
@@ -83,7 +85,17 @@ async def async_send_text_commands(
) as assistant:
command_response_list = []
for command in commands:
resp = await hass.async_add_executor_job(assistant.assist, command)
try:
resp = await hass.async_add_executor_job(assistant.assist, command)
except RpcError as err:
_LOGGER.error(
"Failed to send command '%s' to Google Assistant: %s",
command,
err,
)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="grpc_error"
) from err
text_response = resp[0]
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
audio_response = resp[2]
@@ -0,0 +1,61 @@
"""Support for Google Assistant SDK."""
from __future__ import annotations
import dataclasses
import voluptuous as vol
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN
from .helpers import async_send_text_commands
SERVICE_SEND_TEXT_COMMAND = "send_text_command"
SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command"
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player"
SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All(
{
vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All(
cv.ensure_list, [vol.All(str, vol.Length(min=1))]
),
vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids,
},
)
async def _send_text_command(call: ServiceCall) -> ServiceResponse:
"""Send a text command to Google Assistant SDK."""
commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND]
media_players: list[str] | None = call.data.get(
SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER
)
command_response_list = await async_send_text_commands(
call.hass, commands, media_players
)
if call.return_response:
return {
"responses": [
dataclasses.asdict(command_response)
for command_response in command_response_list
]
}
return None
def async_setup_services(hass: HomeAssistant) -> None:
"""Add the services for Google Assistant SDK."""
hass.services.async_register(
DOMAIN,
SERVICE_SEND_TEXT_COMMAND,
_send_text_command,
schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
@@ -57,5 +57,10 @@
}
}
}
},
"exceptions": {
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
}
}
}
+66 -1
View File
@@ -37,6 +37,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery_flow,
issue_registry as ir,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.deprecation import (
@@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import (
get_supervisor_ip as _get_supervisor_ip,
is_hassio as _is_hassio,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service_info.hassio import (
HassioServiceInfo as _HassioServiceInfo,
)
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from homeassistant.util.async_ import create_eager_task
@@ -109,7 +112,7 @@ from .coordinator import (
get_core_info, # noqa: F401
get_core_stats, # noqa: F401
get_host_info, # noqa: F401
get_info, # noqa: F401
get_info,
get_issues_info, # noqa: F401
get_os_info,
get_supervisor_info, # noqa: F401
@@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
DEPRECATION_URL = (
"https://www.home-assistant.io/blog/2025/05/22/"
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
)
def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug."""
@@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data[ADDONS_COORDINATOR] = coordinator
system_info = await async_get_system_info(hass)
def deprecated_setup_issue() -> None:
os_info = get_os_info(hass)
info = get_info(hass)
if os_info is None or info is None:
return
is_haos = info.get("hassos") is not None
arch = system_info["arch"]
board = os_info.get("board")
supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"}
if is_haos and arch == "armv7" and supported_board:
issue_id = "deprecated_os_"
if board in {"rpi3", "rpi4"}:
issue_id += "aarch64"
elif board in {"tinker", "odroid-xu4", "rpi2"}:
issue_id += "armv7"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_guide": "https://www.home-assistant.io/installation/",
},
)
deprecated_architecture = False
if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board):
deprecated_architecture = True
if not is_haos or deprecated_architecture:
issue_id = "deprecated"
if not is_haos:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": "OS" if is_haos else "Supervised",
"arch": arch,
},
)
listener()
listener = coordinator.async_add_listener(deprecated_setup_issue)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
+1 -1
View File
@@ -144,5 +144,5 @@ class SupervisorEntityModel(StrEnum):
ADDON = "Home Assistant Add-on"
OS = "Home Assistant Operating System"
CORE = "Home Assistant Core"
SUPERVIOSR = "Home Assistant Supervisor"
SUPERVISOR = "Home Assistant Supervisor"
HOST = "Home Assistant Host"
@@ -261,7 +261,7 @@ def async_register_supervisor_in_dev_reg(
params = DeviceInfo(
identifiers={(DOMAIN, "supervisor")},
manufacturer="Home Assistant",
model=SupervisorEntityModel.SUPERVIOSR,
model=SupervisorEntityModel.SUPERVISOR,
sw_version=supervisor_dict[ATTR_VERSION],
name="Home Assistant Supervisor",
entry_type=dr.DeviceEntryType.SERVICE,
+8 -8
View File
@@ -25,17 +25,12 @@ def _get_obj_holidays_and_language(
selected_categories: list[str] | None,
) -> tuple[HolidayBase, str]:
"""Get the object for the requested country and year."""
if selected_categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *selected_categories]
obj_holidays = country_holidays(
country,
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=language,
categories=categories,
categories=selected_categories,
)
if language == "en":
for lang in obj_holidays.supported_languages:
@@ -45,7 +40,7 @@ def _get_obj_holidays_and_language(
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=lang,
categories=categories,
categories=selected_categories,
)
language = lang
break
@@ -59,7 +54,7 @@ def _get_obj_holidays_and_language(
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=default_language,
categories=categories,
categories=selected_categories,
)
language = default_language
@@ -77,6 +72,11 @@ async def async_setup_entry(
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
language = hass.config.language
if categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *categories]
obj_holidays, language = await hass.async_add_executor_job(
_get_obj_holidays_and_language, country, province, language, categories
)
@@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
"developer_dashboard_url": "https://developer.home-connect.com/",
"applications_url": "https://developer.home-connect.com/applications",
"register_application_url": "https://developer.home-connect.com/application/add",
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
}
@@ -1,4 +1,7 @@
{
"application_credentials": {
"description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**."
},
"common": {
"confirmed": "Confirmed",
"present": "Present"
@@ -13,7 +16,7 @@
"description": "The Home Connect integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect."
"description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect."
}
},
"abort": {
@@ -4,7 +4,7 @@ import asyncio
from collections.abc import Callable, Coroutine
import itertools as it
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
import voluptuous as vol
@@ -38,7 +38,6 @@ from homeassistant.helpers import (
restore_state,
)
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service import (
async_extract_config_entry_ids,
@@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
info = await async_get_system_info(hass)
installation_type = info["installation_type"][15:]
deprecated_method = installation_type in {
"Core",
"Supervised",
}
arch = info["arch"]
if arch == "armv7":
if installation_type == "OS":
# Local import to avoid circular dependencies
# We use the import helper because hassio
# may not be loaded yet and we don't want to
# do blocking I/O in the event loop to import it.
if TYPE_CHECKING:
# pylint: disable-next=import-outside-toplevel
from homeassistant.components import hassio
else:
hassio = await async_import_module(
hass, "homeassistant.components.hassio"
)
os_info = hassio.get_os_info(hass)
assert os_info is not None
issue_id = "deprecated_os_"
board = os_info.get("board")
if board in {"rpi3", "rpi4"}:
issue_id += "aarch64"
elif board in {"tinker", "odroid-xu4", "rpi2"}:
issue_id += "armv7"
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_guide": "https://www.home-assistant.io/installation/",
},
)
elif installation_type == "Container":
if installation_type in {"Core", "Container"}:
deprecated_method = installation_type == "Core"
arch = info["arch"]
if arch == "armv7" and installation_type == "Container":
ir.async_create_issue(
hass,
DOMAIN,
@@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
severity=IssueSeverity.WARNING,
translation_key="deprecated_container_armv7",
)
deprecated_architecture = False
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
deprecated_architecture = True
if deprecated_method or deprecated_architecture:
issue_id = "deprecated"
if deprecated_method:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": installation_type,
"arch": arch,
},
)
deprecated_architecture = False
if arch in {"i386", "armhf"} or (
arch == "armv7" and installation_type != "Container"
):
deprecated_architecture = True
if deprecated_method or deprecated_architecture:
issue_id = "deprecated"
if deprecated_method:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
breaks_in_ha_version="2025.12.0",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": installation_type,
"arch": arch,
},
)
return True
@@ -20,11 +20,11 @@
},
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries."
},
"deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant."
},
"historic_currency": {
"title": "The configured currency is no longer in use",
+23 -9
View File
@@ -83,7 +83,7 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
self._heating_mode is not None
):
if self._heating_mode.current_value == 0:
if self._heating_mode.current_value == self._heating_mode.minimum:
return HVACMode.OFF
return HVACMode.HEAT
@@ -91,7 +91,10 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
@property
def hvac_action(self) -> HVACAction:
"""Return the hvac action."""
if self._heating_mode is not None and self._heating_mode.current_value == 0:
if (
self._heating_mode is not None
and self._heating_mode.current_value == self._heating_mode.minimum
):
return HVACAction.OFF
if (
@@ -110,10 +113,12 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
if (
ClimateEntityFeature.PRESET_MODE in self.supported_features
and self._heating_mode is not None
and self._heating_mode.current_value > 0
and self._heating_mode.current_value > self._heating_mode.minimum
):
assert self._attr_preset_modes is not None
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
return self._attr_preset_modes[
int(self._heating_mode.current_value - self._heating_mode.minimum) - 1
]
return PRESET_NONE
@@ -147,14 +152,16 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
# Currently only HEAT and OFF are supported.
assert self._heating_mode is not None
await self.async_set_homee_value(
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
self._heating_mode,
(hvac_mode == HVACMode.HEAT) + self._heating_mode.minimum,
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new target preset mode."""
assert self._heating_mode is not None and self._attr_preset_modes is not None
await self.async_set_homee_value(
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
self._heating_mode,
self._attr_preset_modes.index(preset_mode) + self._heating_mode.minimum + 1,
)
async def async_set_temperature(self, **kwargs: Any) -> None:
@@ -168,12 +175,16 @@ class HomeeClimate(HomeeNodeEntity, ClimateEntity):
async def async_turn_on(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 1)
await self.async_set_homee_value(
self._heating_mode, 1 + self._heating_mode.minimum
)
async def async_turn_off(self) -> None:
"""Turn the entity on."""
assert self._heating_mode is not None
await self.async_set_homee_value(self._heating_mode, 0)
await self.async_set_homee_value(
self._heating_mode, 0 + self._heating_mode.minimum
)
def get_climate_features(
@@ -193,7 +204,10 @@ def get_climate_features(
if attribute.maximum > 1:
# Node supports more modes than off and heating.
features |= ClimateEntityFeature.PRESET_MODE
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
if attribute.maximum < 5:
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
else:
preset_modes.extend([PRESET_ECO])
if len(preset_modes) > 0:
preset_modes.insert(0, PRESET_NONE)
@@ -0,0 +1,43 @@
"""Diagnostics for homee integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from . import DOMAIN, HomeeConfigEntry
TO_REDACT = [CONF_PASSWORD, CONF_USERNAME, "latitude", "longitude", "wlan_ssid"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: HomeeConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"settings": async_redact_data(entry.runtime_data.settings.raw_data, TO_REDACT),
"devices": [{"node": node.raw_data} for node in entry.runtime_data.nodes],
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: HomeeConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
# Extract node_id from the device identifiers
split_uid = next(
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
).split("-")
# Homee hub itself only has MAC as identifier and a node_id of -1
node_id = -1 if len(split_uid) < 2 else split_uid[1]
node = entry.runtime_data.get_node_by_id(int(node_id))
assert node is not None
return {
"homee node": node.raw_data,
}
+29
View File
@@ -31,6 +31,22 @@ class HomeeNumberEntityDescription(NumberEntityDescription):
NUMBER_DESCRIPTIONS = {
AttributeType.BUTTON_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
key="button_brightness_active",
entity_category=EntityCategory.CONFIG,
),
AttributeType.BUTTON_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
key="button_brightness_dimmed",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DISPLAY_BRIGHTNESS_ACTIVE: HomeeNumberEntityDescription(
key="display_brightness_active",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DISPLAY_BRIGHTNESS_DIMMED: HomeeNumberEntityDescription(
key="display_brightness_dimmed",
entity_category=EntityCategory.CONFIG,
),
AttributeType.DOWN_POSITION: HomeeNumberEntityDescription(
key="down_position",
entity_category=EntityCategory.CONFIG,
@@ -48,6 +64,14 @@ NUMBER_DESCRIPTIONS = {
key="endposition_configuration",
entity_category=EntityCategory.CONFIG,
),
AttributeType.EXTERNAL_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
key="external_temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.FLOOR_TEMPERATURE_OFFSET: HomeeNumberEntityDescription(
key="floor_temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.MOTION_ALARM_CANCELATION_DELAY: HomeeNumberEntityDescription(
key="motion_alarm_cancelation_delay",
device_class=NumberDeviceClass.DURATION,
@@ -83,6 +107,11 @@ NUMBER_DESCRIPTIONS = {
key="temperature_offset",
entity_category=EntityCategory.CONFIG,
),
AttributeType.TEMPERATURE_REPORT_INTERVAL: HomeeNumberEntityDescription(
key="temperature_report_interval",
device_class=NumberDeviceClass.DURATION,
entity_category=EntityCategory.CONFIG,
),
AttributeType.UP_TIME: HomeeNumberEntityDescription(
key="up_time",
device_class=NumberDeviceClass.DURATION,
+5
View File
@@ -14,6 +14,11 @@ from .entity import HomeeEntity
PARALLEL_UPDATES = 0
SELECT_DESCRIPTIONS: dict[AttributeType, SelectEntityDescription] = {
AttributeType.DISPLAY_TEMPERATURE_SELECTION: SelectEntityDescription(
key="display_temperature_selection",
options=["target", "current"],
entity_category=EntityCategory.CONFIG,
),
AttributeType.REPEATER_MODE: SelectEntityDescription(
key="repeater_mode",
options=["off", "level1", "level2"],
+10
View File
@@ -129,6 +129,16 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
AttributeType.EXTERNAL_TEMPERATURE: HomeeSensorEntityDescription(
key="external_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.FLOOR_TEMPERATURE: HomeeSensorEntityDescription(
key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
AttributeType.INDOOR_RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
key="indoor_humidity",
device_class=SensorDeviceClass.HUMIDITY,
@@ -199,6 +199,18 @@
}
},
"number": {
"button_brightness_active": {
"name": "Button brightness (active)"
},
"button_brightness_dimmed": {
"name": "Button brightness (dimmed)"
},
"display_brightness_active": {
"name": "Display brightness (active)"
},
"display_brightness_dimmed": {
"name": "Display brightness (dimmed)"
},
"down_position": {
"name": "Down position"
},
@@ -211,6 +223,12 @@
"endposition_configuration": {
"name": "End position"
},
"external_temperature_offset": {
"name": "External temperature offset"
},
"floor_temperature_offset": {
"name": "Floor temperature offset"
},
"motion_alarm_cancelation_delay": {
"name": "Motion alarm delay"
},
@@ -235,6 +253,9 @@
"temperature_offset": {
"name": "Temperature offset"
},
"temperature_report_interval": {
"name": "Temperature report interval"
},
"up_time": {
"name": "Up-movement duration"
},
@@ -246,6 +267,13 @@
}
},
"select": {
"display_temperature_selection": {
"name": "Displayed temperature",
"state": {
"target": "Target",
"current": "Measured"
}
},
"repeater_mode": {
"name": "Repeater mode",
"state": {
@@ -277,6 +305,12 @@
"exhaust_motor_revs": {
"name": "Exhaust motor speed"
},
"external_temperature": {
"name": "External temperature"
},
"floor_temperature": {
"name": "Floor temperature"
},
"indoor_humidity": {
"name": "Indoor humidity"
},
@@ -112,6 +112,7 @@ class HomematicipHAP:
self.config_entry = config_entry
self._ws_close_requested = False
self._ws_connection_closed = asyncio.Event()
self._retry_task: asyncio.Task | None = None
self._tries = 0
self._accesspoint_connected = True
@@ -218,6 +219,8 @@ class HomematicipHAP:
try:
await self.home.get_current_state_async()
hmip_events = self.home.enable_events()
self.home.set_on_connected_handler(self.ws_connected_handler)
self.home.set_on_disconnected_handler(self.ws_disconnected_handler)
tries = 0
await hmip_events
except HmipConnectionError:
@@ -267,6 +270,18 @@ class HomematicipHAP:
"Reset connection to access point id %s", self.config_entry.unique_id
)
async def ws_connected_handler(self) -> None:
"""Handle websocket connected."""
_LOGGER.debug("WebSocket connection to HomematicIP established")
if self._ws_connection_closed.is_set():
await self.get_state()
self._ws_connection_closed.clear()
async def ws_disconnected_handler(self) -> None:
"""Handle websocket disconnection."""
_LOGGER.warning("WebSocket connection to HomematicIP closed")
self._ws_connection_closed.set()
async def get_hap(
self,
hass: HomeAssistant,
@@ -290,6 +305,7 @@ class HomematicipHAP:
raise HmipcConnectionError from err
home.on_update(self.async_update)
home.on_create(self.async_create_entity)
hass.loop.create_task(self.async_connect())
return home
@@ -4,16 +4,16 @@ from __future__ import annotations
from typing import Any
from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState
from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState
from homematicip.base.functionalChannels import NotificationLightChannel
from homematicip.device import (
BrandDimmer,
BrandSwitchMeasuring,
BrandSwitchNotificationLight,
Dimmer,
DinRailDimmer3,
FullFlushDimmer,
PluggableDimmer,
SwitchMeasuring,
WiredDimmer3,
)
from packaging.version import Version
@@ -44,9 +44,12 @@ async def async_setup_entry(
hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices:
if isinstance(device, BrandSwitchMeasuring):
if (
isinstance(device, SwitchMeasuring)
and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING
):
entities.append(HomematicipLightMeasuring(hap, device))
elif isinstance(device, BrandSwitchNotificationLight):
if isinstance(device, BrandSwitchNotificationLight):
device_version = Version(device.firmwareVersion)
entities.append(HomematicipLight(hap, device))
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.1.1"]
"requirements": ["homematicip==2.0.4"]
}
@@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import (
FunctionalChannel,
)
from homematicip.device import (
BrandSwitchMeasuring,
EnergySensorsInterface,
FloorTerminalBlock6,
FloorTerminalBlock10,
FloorTerminalBlock12,
FullFlushSwitchMeasuring,
HeatingThermostat,
HeatingThermostatCompact,
HeatingThermostatEvo,
@@ -26,9 +24,9 @@ from homematicip.device import (
MotionDetectorOutdoor,
MotionDetectorPushButton,
PassageDetector,
PlugableSwitchMeasuring,
PresenceDetectorIndoor,
RoomControlDeviceAnalog,
SwitchMeasuring,
TemperatureDifferenceSensor2,
TemperatureHumiditySensorDisplay,
TemperatureHumiditySensorOutdoor,
@@ -143,14 +141,7 @@ async def async_setup_entry(
),
):
entities.append(HomematicipIlluminanceSensor(hap, device))
if isinstance(
device,
(
PlugableSwitchMeasuring,
BrandSwitchMeasuring,
FullFlushSwitchMeasuring,
),
):
if isinstance(device, SwitchMeasuring):
entities.append(HomematicipPowerSensor(hap, device))
entities.append(HomematicipEnergySensor(hap, device))
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):
@@ -4,20 +4,19 @@ from __future__ import annotations
from typing import Any
from homematicip.base.enums import DeviceType
from homematicip.device import (
BrandSwitch2,
BrandSwitchMeasuring,
DinRailSwitch,
DinRailSwitch4,
FullFlushInputSwitch,
FullFlushSwitchMeasuring,
HeatingSwitch2,
MultiIOBox,
OpenCollector8Module,
PlugableSwitch,
PlugableSwitchMeasuring,
PrintedCircuitBoardSwitch2,
PrintedCircuitBoardSwitchBattery,
SwitchMeasuring,
WiredSwitch8,
)
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
@@ -43,12 +42,10 @@ async def async_setup_entry(
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
]
for device in hap.home.devices:
if isinstance(device, BrandSwitchMeasuring):
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring
# This entity is implemented in the light platform and will
# not be added in the switch platform
pass
elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)):
if (
isinstance(device, SwitchMeasuring)
and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING
):
entities.append(HomematicipSwitchMeasuring(hap, device))
elif isinstance(device, WiredSwitch8):
entities.extend(
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.3.0"]
"requirements": ["pydrawise==2025.6.0"]
}
@@ -0,0 +1,40 @@
"""Imeon inverter base class for entities."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import InverterCoordinator
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
class InverterEntity(CoordinatorEntity[InverterCoordinator]):
"""Common elements for all entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: InverterCoordinator,
entry: InverterConfigEntry,
entity_description: EntityDescription,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._inverter = coordinator.api.inverter
self.data_key = entity_description.key
assert entry.unique_id
self._attr_unique_id = f"{entry.unique_id}_{self.data_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.unique_id)},
name="Imeon inverter",
manufacturer="Imeon Energy",
model=self._inverter.get("inverter"),
sw_version=self._inverter.get("software"),
serial_number=self._inverter.get("serial"),
configuration_url=self._inverter.get("url"),
)
@@ -21,20 +21,18 @@ from homeassistant.const import (
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import InverterCoordinator
from .entity import InverterEntity
type InverterConfigEntry = ConfigEntry[InverterCoordinator]
_LOGGER = logging.getLogger(__name__)
ENTITY_DESCRIPTIONS = (
SENSOR_DESCRIPTIONS = (
# Battery
SensorEntityDescription(
key="battery_autonomy",
@@ -423,42 +421,18 @@ async def async_setup_entry(
"""Create each sensor for a given config entry."""
coordinator = entry.runtime_data
# Init sensor entities
async_add_entities(
InverterSensor(coordinator, entry, description)
for description in ENTITY_DESCRIPTIONS
for description in SENSOR_DESCRIPTIONS
)
class InverterSensor(CoordinatorEntity[InverterCoordinator], SensorEntity):
"""A sensor that returns numerical values with units."""
class InverterSensor(InverterEntity, SensorEntity):
"""Representation of an Imeon inverter sensor."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self,
coordinator: InverterCoordinator,
entry: InverterConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self.entity_description = description
self._inverter = coordinator.api.inverter
self.data_key = description.key
assert entry.unique_id
self._attr_unique_id = f"{entry.unique_id}_{self.data_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.unique_id)},
name="Imeon inverter",
manufacturer="Imeon Energy",
model=self._inverter.get("inverter"),
sw_version=self._inverter.get("software"),
)
@property
def native_value(self) -> StateType | None:
"""Value of the sensor."""
"""Return the state of the entity."""
return self.coordinator.data.get(self.data_key)
@@ -329,8 +329,8 @@ class JellyfinSource(MediaSource):
movies = await self._get_children(library_id, ITEM_TYPE_MOVIE)
movies = sorted(
movies,
# Sort by whether a movies has an name first, then by name
# This allows for sorting moveis with, without and with missing names
# Sort by whether a movie has a name first, then by name
# This allows for sorting movies with, without and with missing names
key=lambda k: (
ITEM_KEY_NAME not in k,
k.get(ITEM_KEY_NAME),
@@ -388,7 +388,7 @@ class JellyfinSource(MediaSource):
series = await self._get_children(library_id, ITEM_TYPE_SERIES)
series = sorted(
series,
# Sort by whether a seroes has an name first, then by name
# Sort by whether a series has a name first, then by name
# This allows for sorting series with, without and with missing names
key=lambda k: (
ITEM_KEY_NAME not in k,
@@ -225,7 +225,7 @@ async def async_setup_entry(
JewishCalendarTimeSensor(config_entry, description)
for description in TIME_SENSORS
)
async_add_entities(sensors)
async_add_entities(sensors, update_before_add=True)
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
@@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
await self.async_update_data()
async def async_update_data(self) -> None:
async def async_update(self) -> None:
"""Update the state of the sensor."""
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.9.5"]
"requirements": ["aiomealie==0.9.6"]
}
+1
View File
@@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = {
116: "custom_program_20",
323: "pyrolytic",
326: "descale",
327: "evaporate_water",
335: "shabbat_program",
336: "yom_tov",
356: "defrost",
@@ -542,6 +542,7 @@
"endive_strips": "Endive (strips)",
"espresso": "Espresso",
"espresso_macchiato": "Espresso macchiato",
"evaporate_water": "Evaporate water",
"express": "Express",
"express_20": "Express 20'",
"extra_quiet": "Extra quiet",
+2 -2
View File
@@ -293,8 +293,8 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Sensor state value."""
device_point = self.coordinator.data.points[self.device_id][self.point_id]
if device_point.value == MARKER_FOR_UNKNOWN_VALUE:
device_point = self.coordinator.data.points[self.device_id].get(self.point_id)
if device_point is None or device_point.value == MARKER_FOR_UNKNOWN_VALUE:
return None
return device_point.value # type: ignore[no-any-return]
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/neato",
"iot_class": "cloud_polling",
"loggers": ["pybotvac"],
"requirements": ["pybotvac==0.0.26"]
"requirements": ["pybotvac==0.0.28"]
}
@@ -31,7 +31,6 @@ from homeassistant.helpers.selector import (
SelectSelectorConfig,
SelectSelectorMode,
)
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.util import get_random_string
from . import api
@@ -441,10 +440,3 @@ class NestFlowHandler(
if self._structure_config_title:
title = self._structure_config_title
return self.async_create_entry(title=title, data=self._data)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow initialized by discovery."""
await self._async_handle_discovery_without_unique_id()
return await self.async_step_user()
@@ -47,6 +47,9 @@
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Nest integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Google Nest device on your network. Be aware that the set up of Google Nest is more complicated than most other integrations and requires setting up a Nest Device Access project which **requires paying Google a US $5 fee**. Press **Submit** to continue setting up Google Nest."
}
},
"error": {
@@ -1,8 +1,8 @@
"""NextBus data update coordinator."""
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import Any
from typing import Any, override
from py_nextbus import NextBusClient
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
@@ -15,8 +15,14 @@ from .util import RouteStop
_LOGGER = logging.getLogger(__name__)
# At what percentage of the request limit should the coordinator pause making requests
UPDATE_INTERVAL_SECONDS = 30
THROTTLE_PRECENTAGE = 80
class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
class NextBusDataUpdateCoordinator(
DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]]
):
"""Class to manage fetching NextBus data."""
def __init__(self, hass: HomeAssistant, agency: str) -> None:
@@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
_LOGGER,
config_entry=None, # It is shared between multiple entries
name=DOMAIN,
update_interval=timedelta(seconds=30),
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
)
self.client = NextBusClient(agency_id=agency)
self._agency = agency
@@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
"""Check if this coordinator is tracking any routes."""
return len(self._route_stops) > 0
async def _async_update_data(self) -> dict[str, Any]:
@override
async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]:
"""Fetch data from NextBus."""
if (
# If we have predictions, check the rate limit
self._predictions
# If are over our rate limit percentage, we should throttle
and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE
# But only if we have a reset time to unthrottle
and self.client.rate_limit_reset is not None
# Unless we are after the reset time
and datetime.now() < self.client.rate_limit_reset
):
self.logger.debug(
"Rate limit threshold reached. Skipping updates for. Routes: %s",
str(self._route_stops),
)
return self._predictions
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
for route_stop in self._route_stops:
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
@@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
)
def _update_data() -> dict:
def _update_data() -> dict[RouteStop, dict[str, Any]]:
"""Fetch data from NextBus."""
self.logger.debug("Updating data from API (executor)")
predictions: dict[RouteStop, dict[str, Any]] = {}
@@ -8,6 +8,6 @@
"iot_class": "cloud_polling",
"loggers": ["pynordpool"],
"quality_scale": "platinum",
"requirements": ["pynordpool==0.2.4"],
"requirements": ["pynordpool==0.3.0"],
"single_config_entry": true
}
@@ -49,6 +49,10 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
@callback
def _handle_coordinator_update(self) -> None:
"""Update the entity from the latest data."""
self._update_attrs()
super()._handle_coordinator_update()
def _update_attrs(self) -> None:
data = self.coordinator.data
for key in ("from_time", "to_time", "from_uv", "to_uv"):
@@ -78,5 +82,3 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt),
}
)
super()._handle_coordinator_update()
@@ -31,3 +31,8 @@ class OpenUvEntity(CoordinatorEntity):
name="OpenUV",
entry_type=DeviceEntryType.SERVICE,
)
self._update_attrs()
def _update_attrs(self) -> None:
"""Override point for updating attributes during init."""
@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
PERCENTAGE,
UV_INDEX,
@@ -170,7 +169,7 @@ AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
),
SensorEntityDescription(
key=ATTR_API_AIRPOLLUTION_CO,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
),
+1 -1
View File
@@ -96,7 +96,7 @@ class OverseerrSensor(OverseerrEntity, SensorEntity):
coordinator: OverseerrCoordinator,
description: OverseerrSensorEntityDescription,
) -> None:
"""Initialize airgradient sensor."""
"""Initialize Overseerr sensor."""
super().__init__(coordinator, description.key)
self.entity_description = description
self._attr_translation_key = description.key
+1 -1
View File
@@ -8,4 +8,4 @@ from typing import Final
DOMAIN: Final = "powerfox"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=1)
SCAN_INTERVAL = timedelta(seconds=10)
+58 -17
View File
@@ -3,8 +3,10 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import timedelta
import logging
from time import time
from typing import Any
from reolink_aio.api import RETRY_ATTEMPTS
@@ -28,7 +30,13 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .const import (
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
CONF_BC_PORT,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
)
from .exceptions import PasswordIncompatible, ReolinkException, UserNotAdmin
from .host import ReolinkHost
from .services import async_setup_services
@@ -220,22 +228,7 @@ async def async_setup_entry(
hass.http.register_view(PlaybackProxyView(hass))
async def refresh(*args: Any) -> None:
"""Request refresh of coordinator."""
await device_coordinator.async_request_refresh()
host.cancel_refresh_privacy_mode = None
def async_privacy_mode_change() -> None:
"""Request update when privacy mode is turned off."""
if host.privacy_mode and not host.api.baichuan.privacy_mode():
# The privacy mode just turned off, give the API 2 seconds to start
if host.cancel_refresh_privacy_mode is None:
host.cancel_refresh_privacy_mode = async_call_later(hass, 2, refresh)
host.privacy_mode = host.api.baichuan.privacy_mode()
host.api.baichuan.register_callback(
"privacy_mode_change", async_privacy_mode_change, 623
)
await register_callbacks(host, device_coordinator, hass)
# ensure host device is setup before connected camera devices that use via_device
device_registry = dr.async_get(hass)
@@ -254,6 +247,51 @@ async def async_setup_entry(
return True
async def register_callbacks(
host: ReolinkHost,
device_coordinator: DataUpdateCoordinator[None],
hass: HomeAssistant,
) -> None:
"""Register update callbacks."""
async def refresh(*args: Any) -> None:
"""Request refresh of coordinator."""
await device_coordinator.async_request_refresh()
host.cancel_refresh_privacy_mode = None
def async_privacy_mode_change() -> None:
"""Request update when privacy mode is turned off."""
if host.privacy_mode and not host.api.baichuan.privacy_mode():
# The privacy mode just turned off, give the API 2 seconds to start
if host.cancel_refresh_privacy_mode is None:
host.cancel_refresh_privacy_mode = async_call_later(hass, 2, refresh)
host.privacy_mode = host.api.baichuan.privacy_mode()
def generate_async_camera_wake(channel: int) -> Callable[[], None]:
def async_camera_wake() -> None:
"""Request update when a battery camera wakes up."""
if (
not host.api.sleeping(channel)
and time() - host.last_wake[channel]
> BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
):
hass.loop.create_task(device_coordinator.async_request_refresh())
return async_camera_wake
host.api.baichuan.register_callback(
"privacy_mode_change", async_privacy_mode_change, 623
)
for channel in host.api.channels:
if host.api.supported(channel, "battery"):
host.api.baichuan.register_callback(
f"camera_{channel}_wake",
generate_async_camera_wake(channel),
145,
channel,
)
async def entry_update_listener(
hass: HomeAssistant, config_entry: ReolinkConfigEntry
) -> None:
@@ -270,6 +308,9 @@ async def async_unload_entry(
await host.stop()
host.api.baichuan.unregister_callback("privacy_mode_change")
for channel in host.api.channels:
if host.api.supported(channel, "battery"):
host.api.baichuan.unregister_callback(f"camera_{channel}_wake")
if host.cancel_refresh_privacy_mode is not None:
host.cancel_refresh_privacy_mode()
@@ -5,3 +5,9 @@ DOMAIN = "reolink"
CONF_USE_HTTPS = "use_https"
CONF_BC_PORT = "baichuan_port"
CONF_SUPPORTS_PRIVACY_MODE = "privacy_mode_supported"
# 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_PASSIVE_WAKE_UPDATE_INTERVAL = 3600 # seconds
BATTERY_WAKE_UPDATE_INTERVAL = 6 * BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
BATTERY_ALL_WAKE_UPDATE_INTERVAL = 2 * BATTERY_WAKE_UPDATE_INTERVAL
+3 -1
View File
@@ -142,7 +142,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
async def async_update(self) -> None:
"""Force full update from the generic entity update service."""
self._host.last_wake = 0
for channel in self._host.api.channels:
if self._host.api.supported(channel, "battery"):
self._host.last_wake[channel] = 0
await super().async_update()
+35 -11
View File
@@ -34,7 +34,15 @@ from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.storage import Store
from homeassistant.util.ssl import SSLCipherList
from .const import CONF_BC_PORT, CONF_SUPPORTS_PRIVACY_MODE, CONF_USE_HTTPS, DOMAIN
from .const import (
BATTERY_ALL_WAKE_UPDATE_INTERVAL,
BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL,
BATTERY_WAKE_UPDATE_INTERVAL,
CONF_BC_PORT,
CONF_SUPPORTS_PRIVACY_MODE,
CONF_USE_HTTPS,
DOMAIN,
)
from .exceptions import (
PasswordIncompatible,
ReolinkSetupException,
@@ -52,10 +60,6 @@ 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__)
@@ -95,7 +99,8 @@ class ReolinkHost:
bc_port=config.get(CONF_BC_PORT, DEFAULT_BC_PORT),
)
self.last_wake: float = 0
self.last_wake: defaultdict[int, float] = defaultdict(float)
self.last_all_wake: float = 0
self.update_cmd: defaultdict[str, defaultdict[int | None, int]] = defaultdict(
lambda: defaultdict(int)
)
@@ -459,15 +464,34 @@ class ReolinkHost:
async def update_states(self) -> None:
"""Call the API of the camera device to update the internal states."""
wake = False
if time() - self.last_wake > BATTERY_WAKE_UPDATE_INTERVAL:
wake: dict[int, bool] = {}
now = time()
for channel in self._api.stream_channels:
# wake the battery cameras for a complete update
wake = True
self.last_wake = time()
if not self._api.supported(channel, "battery"):
wake[channel] = True
elif (
(
not self._api.sleeping(channel)
and now - self.last_wake[channel]
> BATTERY_PASSIVE_WAKE_UPDATE_INTERVAL
)
or (now - self.last_wake[channel] > BATTERY_WAKE_UPDATE_INTERVAL)
or (now - self.last_all_wake > BATTERY_ALL_WAKE_UPDATE_INTERVAL)
):
# let a waking update coincide with the camera waking up by itself unless it did not wake for BATTERY_WAKE_UPDATE_INTERVAL
wake[channel] = True
self.last_wake[channel] = now
else:
wake[channel] = False
for channel in self._api.channels:
# check privacy mode if enabled
if self._api.baichuan.privacy_mode(channel):
await self._api.baichuan.get_privacy_mode(channel)
if all(wake.values()):
self.last_all_wake = now
if self._api.baichuan.privacy_mode():
return # API is shutdown, no need to check states
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["sensorpush_api", "sensorpush_ha"],
"quality_scale": "bronze",
"requirements": ["sensorpush-api==2.1.2", "sensorpush-ha==1.3.2"]
"requirements": ["sensorpush-api==2.1.3", "sensorpush-ha==1.3.2"]
}
@@ -30,5 +30,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.3"]
"requirements": ["pysmartthings==3.2.4"]
}
@@ -619,15 +619,6 @@
"keep_fresh_mode": {
"name": "Keep fresh mode"
}
},
"water_heater": {
"water_heater": {
"state": {
"standard": "Standard",
"force": "Forced",
"power": "Power"
}
}
}
},
"issues": {
@@ -10,6 +10,9 @@ from homeassistant.components.water_heater import (
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
STATE_ECO,
STATE_HEAT_PUMP,
STATE_HIGH_DEMAND,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
@@ -24,9 +27,9 @@ from .entity import SmartThingsEntity
OPERATION_MAP_TO_HA: dict[str, str] = {
"eco": STATE_ECO,
"std": "standard",
"force": "force",
"power": "power",
"std": STATE_HEAT_PUMP,
"force": STATE_HIGH_DEMAND,
"power": STATE_PERFORMANCE,
}
HA_TO_OPERATION_MAP = {v: k for k, v in OPERATION_MAP_TO_HA.items()}
+26
View File
@@ -18,6 +18,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import StateType
from .const import DOMAIN, SIGNAL_EVENTS_CHANGED, SIGNAL_POSITION_CHANGED
@@ -149,6 +154,21 @@ class SunSensor(SensorEntity):
async def async_added_to_hass(self) -> None:
"""Register signal listener when added to hass."""
await super().async_added_to_hass()
if self.entity_description.key == "solar_rising":
async_create_issue(
self.hass,
DOMAIN,
"deprecated_sun_solar_rising",
breaks_in_ha_version="2026.1.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_sun_solar_rising",
translation_placeholders={
"entity": self.entity_id,
},
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@@ -156,3 +176,9 @@ class SunSensor(SensorEntity):
self.async_write_ha_state,
)
)
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
await super().async_will_remove_from_hass()
if self.entity_description.key == "solar_rising":
async_delete_issue(self.hass, DOMAIN, "deprecated_sun_solar_rising")
@@ -37,5 +37,11 @@
}
}
}
},
"issues": {
"deprecated_sun_solar_rising": {
"title": "Deprecated 'Solar rising' sensor",
"description": "The 'Solar rising' sensor of the Sun integration is being deprecated; an equivalent 'Solar rising' binary sensor has been made available as a replacement. To resolve this issue, disable {entity}."
}
}
}
@@ -9,14 +9,11 @@ import voluptuous as vol
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from .const import CONF_INVERT, CONF_TARGET_DOMAIN
from .light import LightSwitch
__all__ = ["LightSwitch"]
_LOGGER = logging.getLogger(__name__)
@@ -44,10 +41,11 @@ def async_add_to_device(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
registry = er.async_get(hass)
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
try:
entity_id = er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID])
entity_id = er.async_validate_entity_id(
entity_registry, entry.options[CONF_ENTITY_ID]
)
except vol.Invalid:
# The entity is identified by an unknown entity registry ID
_LOGGER.error(
@@ -56,45 +54,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return False
async def async_registry_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] == "remove":
await hass.config_entries.async_remove(entry.entry_id)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
)
if data["action"] != "update":
return
if "entity_id" in data["changes"]:
# Entity_id changed, reload the config entry
await hass.config_entries.async_reload(entry.entry_id)
if device_id and "device_id" in data["changes"]:
# If the tracked switch is no longer in the device, remove our config entry
# from the device
if (
not (entity_entry := registry.async_get(data[CONF_ENTITY_ID]))
or not device_registry.async_get(device_id)
or entity_entry.device_id == device_id
):
# No need to do any cleanup
return
device_registry.async_update_device(
device_id, remove_config_entry_id=entry.entry_id
)
async def source_entity_removed() -> None:
# The source entity has been removed, we remove the config entry because
# switch_as_x does not allow replacing the wrapped entity.
await hass.config_entries.async_remove(entry.entry_id)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, entity_id, async_registry_updated
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_add_to_device(hass, entry, entity_id),
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
source_entity_removed=source_entity_removed,
)
)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
device_id = async_add_to_device(hass, entry, entity_id)
await hass.config_entries.async_forward_entry_setups(
entry, (entry.options[CONF_TARGET_DOMAIN],)
)
@@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.65.0"]
"requirements": ["PySwitchbot==0.66.0"]
}
@@ -374,6 +374,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
)
elif msgtype == SERVICE_DELETE_MESSAGE:
await notify_service.delete_message(context=service.context, **kwargs)
elif msgtype == SERVICE_LEAVE_CHAT:
messages = await notify_service.leave_chat(
context=service.context, **kwargs
)
else:
await notify_service.edit_message(
msgtype, context=service.context, **kwargs
@@ -447,7 +451,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TelegramBotConfigEntry)
async def update_listener(hass: HomeAssistant, entry: TelegramBotConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
entry.runtime_data.parse_mode = entry.options[ATTR_PARSER]
async def async_unload_entry(
+119 -71
View File
@@ -28,11 +28,12 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_COMMAND,
CONF_API_KEY,
HTTP_BASIC_AUTHENTICATION,
HTTP_BEARER_AUTHENTICATION,
HTTP_DIGEST_AUTHENTICATION,
)
from homeassistant.core import Context, HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import issue_registry as ir
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
@@ -238,9 +239,10 @@ class TelegramNotificationService:
PARSER_MD2: ParseMode.MARKDOWN_V2,
PARSER_PLAIN_TEXT: None,
}
self._parse_mode = self._parsers.get(parser)
self.parse_mode = self._parsers.get(parser)
self.bot = bot
self.hass = hass
self._last_message_id: dict[int, int] = {}
def _get_allowed_chat_ids(self) -> list[int]:
allowed_chat_ids: list[int] = [
@@ -260,9 +262,6 @@ class TelegramNotificationService:
return allowed_chat_ids
def _get_last_message_id(self):
return dict.fromkeys(self._get_allowed_chat_ids())
def _get_msg_ids(self, msg_data, chat_id):
"""Get the message id to edit.
@@ -277,9 +276,9 @@ class TelegramNotificationService:
if (
isinstance(message_id, str)
and (message_id == "last")
and (self._get_last_message_id()[chat_id] is not None)
and (chat_id in self._last_message_id)
):
message_id = self._get_last_message_id()[chat_id]
message_id = self._last_message_id[chat_id]
else:
inline_message_id = msg_data["inline_message_id"]
return message_id, inline_message_id
@@ -352,7 +351,7 @@ class TelegramNotificationService:
# Defaults
params = {
ATTR_PARSER: self._parse_mode,
ATTR_PARSER: self.parse_mode,
ATTR_DISABLE_NOTIF: False,
ATTR_DISABLE_WEB_PREV: None,
ATTR_REPLY_TO_MSGID: None,
@@ -364,7 +363,7 @@ class TelegramNotificationService:
if data is not None:
if ATTR_PARSER in data:
params[ATTR_PARSER] = self._parsers.get(
data[ATTR_PARSER], self._parse_mode
data[ATTR_PARSER], self.parse_mode
)
if ATTR_TIMEOUT in data:
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
@@ -408,10 +407,10 @@ class TelegramNotificationService:
if not isinstance(out, bool) and hasattr(out, ATTR_MESSAGEID):
chat_id = out.chat_id
message_id = out[ATTR_MESSAGEID]
self._get_last_message_id()[chat_id] = message_id
self._last_message_id[chat_id] = message_id
_LOGGER.debug(
"Last message ID: %s (from chat_id %s)",
self._get_last_message_id(),
self._last_message_id,
chat_id,
)
@@ -480,9 +479,9 @@ class TelegramNotificationService:
context=context,
)
# reduce message_id anyway:
if self._get_last_message_id()[chat_id] is not None:
if chat_id in self._last_message_id:
# change last msg_id for deque(n_msgs)?
self._get_last_message_id()[chat_id] -= 1
self._last_message_id[chat_id] -= 1
return deleted
async def edit_message(self, type_edit, chat_id=None, context=None, **kwargs):
@@ -855,70 +854,119 @@ async def load_data(
verify_ssl=None,
):
"""Load data into ByteIO/File container from a source."""
try:
if url is not None:
# Load data from URL
params: dict[str, Any] = {}
headers = {}
if authentication == HTTP_BEARER_AUTHENTICATION and password is not None:
headers = {"Authorization": f"Bearer {password}"}
elif username is not None and password is not None:
if authentication == HTTP_DIGEST_AUTHENTICATION:
params["auth"] = httpx.DigestAuth(username, password)
else:
params["auth"] = httpx.BasicAuth(username, password)
if verify_ssl is not None:
params["verify"] = verify_ssl
if url is not None:
# Load data from URL
params: dict[str, Any] = {}
headers = {}
_validate_credentials_input(authentication, username, password)
if authentication == HTTP_BEARER_AUTHENTICATION:
headers = {"Authorization": f"Bearer {password}"}
elif authentication == HTTP_DIGEST_AUTHENTICATION:
params["auth"] = httpx.DigestAuth(username, password)
elif authentication == HTTP_BASIC_AUTHENTICATION:
params["auth"] = httpx.BasicAuth(username, password)
retry_num = 0
async with httpx.AsyncClient(
timeout=15, headers=headers, **params
) as client:
while retry_num < num_retries:
if verify_ssl is not None:
params["verify"] = verify_ssl
retry_num = 0
async with httpx.AsyncClient(timeout=15, headers=headers, **params) as client:
while retry_num < num_retries:
try:
req = await client.get(url)
if req.status_code != 200:
_LOGGER.warning(
"Status code %s (retry #%s) loading %s",
req.status_code,
retry_num + 1,
url,
)
else:
data = io.BytesIO(req.content)
if data.read():
data.seek(0)
data.name = url
return data
_LOGGER.warning(
"Empty data (retry #%s) in %s)", retry_num + 1, url
)
retry_num += 1
if retry_num < num_retries:
await asyncio.sleep(
1
) # Add a sleep to allow other async operations to proceed
_LOGGER.warning(
"Can't load data in %s after %s retries", url, retry_num
)
elif filepath is not None:
if hass.config.is_allowed_path(filepath):
return await hass.async_add_executor_job(
_read_file_as_bytesio, filepath
)
except (httpx.HTTPError, httpx.InvalidURL) as err:
raise HomeAssistantError(
f"Failed to load URL: {err!s}",
translation_domain=DOMAIN,
translation_key="failed_to_load_url",
translation_placeholders={"error": str(err)},
) from err
_LOGGER.warning("'%s' are not secure to load data from!", filepath)
else:
_LOGGER.warning("Can't load data. No data found in params!")
if req.status_code != 200:
_LOGGER.warning(
"Status code %s (retry #%s) loading %s",
req.status_code,
retry_num + 1,
url,
)
else:
data = io.BytesIO(req.content)
if data.read():
data.seek(0)
data.name = url
return data
_LOGGER.warning("Empty data (retry #%s) in %s)", retry_num + 1, url)
retry_num += 1
if retry_num < num_retries:
await asyncio.sleep(
1
) # Add a sleep to allow other async operations to proceed
raise HomeAssistantError(
f"Failed to load URL: {req.status_code}",
translation_domain=DOMAIN,
translation_key="failed_to_load_url",
translation_placeholders={"error": str(req.status_code)},
)
elif filepath is not None:
if hass.config.is_allowed_path(filepath):
return await hass.async_add_executor_job(_read_file_as_bytesio, filepath)
except (OSError, TypeError) as error:
_LOGGER.error("Can't load data into ByteIO: %s", error)
raise ServiceValidationError(
"File path has not been configured in allowlist_external_dirs.",
translation_domain=DOMAIN,
translation_key="allowlist_external_dirs_error",
)
else:
raise ServiceValidationError(
"URL or File is required.",
translation_domain=DOMAIN,
translation_key="missing_input",
translation_placeholders={"field": "URL or File"},
)
return None
def _validate_credentials_input(
authentication: str | None, username: str | None, password: str | None
) -> None:
if (
authentication in (HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION)
and username is None
):
raise ServiceValidationError(
"Username is required.",
translation_domain=DOMAIN,
translation_key="missing_input",
translation_placeholders={"field": "Username"},
)
if (
authentication
in (
HTTP_BASIC_AUTHENTICATION,
HTTP_BEARER_AUTHENTICATION,
HTTP_BEARER_AUTHENTICATION,
)
and password is None
):
raise ServiceValidationError(
"Password is required.",
translation_domain=DOMAIN,
translation_key="missing_input",
translation_placeholders={"field": "Password"},
)
def _read_file_as_bytesio(file_path: str) -> io.BytesIO:
"""Read a file and return it as a BytesIO object."""
with open(file_path, "rb") as file:
data = io.BytesIO(file.read())
data.name = file_path
return data
try:
with open(file_path, "rb") as file:
data = io.BytesIO(file.read())
data.name = file_path
return data
except OSError as err:
raise HomeAssistantError(
f"Failed to load file: {err!s}",
translation_domain=DOMAIN,
translation_key="failed_to_load_file",
translation_placeholders={"error": str(err)},
) from err
@@ -41,6 +41,9 @@
},
"delete_message": {
"service": "mdi:delete"
},
"leave_chat": {
"service": "mdi:exit-run"
}
}
}
@@ -19,7 +19,7 @@ async def async_setup_platform(
"""Set up the Telegram polling platform."""
pollbot = PollBot(hass, bot, config)
config.async_create_task(hass, pollbot.start_polling(), "polling telegram bot")
await pollbot.start_polling()
return pollbot
@@ -774,3 +774,15 @@ delete_message:
example: 12345
selector:
text:
leave_chat:
fields:
config_entry_id:
selector:
config_entry:
integration: telegram_bot
chat_id:
required: true
example: 12345
selector:
text:
@@ -842,6 +842,20 @@
"description": "ID of the chat where to delete the message."
}
}
},
"leave_chat": {
"name": "Leave chat",
"description": "Removes the bot from the chat.",
"fields": {
"config_entry_id": {
"name": "[%key:component::telegram_bot::services::send_message::fields::config_entry_id::name%]",
"description": "The config entry representing the Telegram bot to leave the chat."
},
"chat_id": {
"name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]",
"description": "Chat ID of the group from which the bot should be removed."
}
}
}
},
"exceptions": {
@@ -853,6 +867,18 @@
},
"missing_allowed_chat_ids": {
"message": "No allowed chat IDs found. Please add allowed chat IDs for {bot_name}."
},
"missing_input": {
"message": "{field} is required."
},
"failed_to_load_url": {
"message": "Failed to load URL: {error}"
},
"allowlist_external_dirs_error": {
"message": "File path has not been configured in allowlist_external_dirs."
},
"failed_to_load_file": {
"message": "Failed to load file: {error}"
}
},
"issues": {
@@ -1,6 +1,5 @@
"""Support for Telegram bots using webhooks."""
import datetime as dt
from http import HTTPStatus
from ipaddress import IPv4Network, ip_address
import logging
@@ -8,7 +7,7 @@ import secrets
import string
from telegram import Bot, Update
from telegram.error import NetworkError, TimedOut
from telegram.error import NetworkError, TelegramError
from telegram.ext import ApplicationBuilder, TypeHandler
from homeassistant.components.http import HomeAssistantView
@@ -98,9 +97,9 @@ class PushBot(BaseTelegramBot):
api_kwargs={"secret_token": self.secret_token},
connect_timeout=5,
)
except TimedOut:
except TelegramError:
retry_num += 1
_LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num)
_LOGGER.warning("Error trying to set webhook (retry #%d)", retry_num)
return False
@@ -113,16 +112,7 @@ class PushBot(BaseTelegramBot):
"""Query telegram and register the URL for our webhook."""
current_status = await self.bot.get_webhook_info()
# Some logging of Bot current status:
last_error_date = getattr(current_status, "last_error_date", None)
if (last_error_date is not None) and (isinstance(last_error_date, int)):
last_error_date = dt.datetime.fromtimestamp(last_error_date)
_LOGGER.debug(
"Telegram webhook last_error_date: %s. Status: %s",
last_error_date,
current_status,
)
else:
_LOGGER.debug("telegram webhook status: %s", current_status)
_LOGGER.debug("telegram webhook status: %s", current_status)
result = await self._try_to_set_webhook()
if result:
+3 -1
View File
@@ -524,8 +524,10 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
ATTR_COLOR_TEMP_KELVIN in kwargs
and (script := CONF_TEMPERATURE_ACTION) in self._action_scripts
):
kelvin = kwargs[ATTR_COLOR_TEMP_KELVIN]
common_params[ATTR_COLOR_TEMP_KELVIN] = kelvin
common_params["color_temp"] = color_util.color_temperature_kelvin_to_mired(
kwargs[ATTR_COLOR_TEMP_KELVIN]
kelvin
)
return (script, common_params)
@@ -195,9 +195,13 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
raise UpdateFailed(e.message) from e
# Add all time periods together
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, 0)
output = dict.fromkeys(ENERGY_HISTORY_FIELDS, None)
for period in data.get("time_series", []):
for key in ENERGY_HISTORY_FIELDS:
output[key] += period.get(key, 0)
if key in period:
if output[key] is None:
output[key] = period[key]
else:
output[key] += period[key]
return output
@@ -239,7 +239,14 @@ class SensorTrend(BinarySensorEntity, RestoreEntity):
self.async_schedule_update_ha_state(True)
except (ValueError, TypeError) as ex:
_LOGGER.error(ex)
_LOGGER.error(
"Error processing sensor state change for "
"entity_id=%s, attribute=%s, state=%s: %s",
self._entity_id,
self._attribute,
new_state.state,
ex,
)
self.async_on_remove(
async_track_state_change_event(
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==7.12.0", "unifi-discovery==1.2.0"],
"requirements": ["uiprotect==7.13.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -37,7 +37,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: VodafoneConfigEntry) ->
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
coordinator = entry.runtime_data
await coordinator.api.logout()
await coordinator.api.close()
return unload_ok
@@ -48,7 +48,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
await api.login()
finally:
await api.logout()
await api.close()
return {"title": data[CONF_HOST]}
@@ -117,32 +117,29 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
async def _async_update_data(self) -> UpdateCoordinatorDataType:
"""Update router data."""
_LOGGER.debug("Polling Vodafone Station host: %s", self._host)
try:
try:
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
except (
exceptions.CannotConnect,
exceptions.AlreadyLogged,
exceptions.GenericLoginError,
JSONDecodeError,
) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": repr(err)},
) from err
except (ConfigEntryAuthFailed, UpdateFailed):
await self.api.close()
raise
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
translation_placeholders={"error": repr(err)},
) from err
except (
exceptions.CannotConnect,
exceptions.AlreadyLogged,
exceptions.GenericLoginError,
JSONDecodeError,
) as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": repr(err)},
) from err
utc_point_in_time = dt_util.utcnow()
data_devices = {
@@ -336,7 +336,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
if self._run_pipeline_task is not None:
_LOGGER.debug("Cancelling running pipeline")
self._run_pipeline_task.cancel()
self._call_end_future.set_result(None)
if not self._call_end_future.done():
self._call_end_future.set_result(None)
self.disconnect()
break
@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/weheat",
"iot_class": "cloud_polling",
"requirements": ["weheat==2025.4.29"]
"requirements": ["weheat==2025.6.10"]
}
@@ -75,3 +75,11 @@ class WithingsLocalOAuth2Implementation(AuthImplementation):
}
)
return {**token, **new_token}
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
"developer_dashboard_url": "https://developer.withings.com/dashboard/welcome",
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
}
@@ -1,4 +1,7 @@
{
"application_credentials": {
"description": "To be able to login to Withings we require a client ID and secret. To acquire them, please follow the following steps.\n\n1. Go to the [Withings Developer Dashboard]({developer_dashboard_url}) and be sure to select the Public Cloud.\n1. Log in with your Withings account.\n1. Select **Create an application**.\n1. Select the checkbox for **Public API integration**.\n1. Select **Development** as target environment.\n1. Fill in an application name and description of your choice.\n1. Fill in `{redirect_url}` for the registered URL. Make sure that you don't press the button to test it.\n1. Fill in the client ID and secret that are now available."
},
"config": {
"step": {
"pick_implementation": {
@@ -9,7 +12,7 @@
"description": "The Withings integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Withings device on your network. Press **Submit** to continue setting up Withings."
"description": "Home Assistant has found a Withings device on your network. Be aware that the setup of Withings is more complicated than many other integrations. Press **Submit** to continue setting up Withings."
}
},
"error": {
+1 -1
View File
@@ -26,5 +26,5 @@
],
"documentation": "https://www.home-assistant.io/integrations/wiz",
"iot_class": "local_push",
"requirements": ["pywizlight==0.6.2"]
"requirements": ["pywizlight==0.6.3"]
}
@@ -29,7 +29,6 @@ from homeassistant.helpers import (
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import trigger
from .config_validation import VALUE_SCHEMA
from .const import (
ATTR_COMMAND_CLASS,
@@ -67,6 +66,8 @@ from .triggers.value_updated import (
ATTR_FROM,
ATTR_TO,
PLATFORM_TYPE as VALUE_UPDATED_PLATFORM_TYPE,
async_attach_trigger as attach_value_updated_trigger,
async_validate_trigger_config as validate_value_updated_trigger_config,
)
# Trigger types
@@ -448,10 +449,10 @@ async def async_attach_trigger(
ATTR_TO,
],
)
zwave_js_config = await trigger.async_validate_trigger_config(
zwave_js_config = await validate_value_updated_trigger_config(
hass, zwave_js_config
)
return await trigger.async_attach_trigger(
return await attach_value_updated_trigger(
hass, zwave_js_config, action, trigger_info
)
+7 -35
View File
@@ -2,45 +2,17 @@
from __future__ import annotations
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
TriggerProtocol,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger
from .triggers import event, value_updated
TRIGGERS = {
"value_updated": value_updated,
"event": event,
event.PLATFORM_TYPE: event.EventTrigger,
value_updated.PLATFORM_TYPE: value_updated.ValueUpdatedTrigger,
}
def _get_trigger_platform(config: ConfigType) -> TriggerProtocol:
"""Return trigger platform."""
platform_split = config[CONF_PLATFORM].split(".", maxsplit=1)
if len(platform_split) < 2 or platform_split[1] not in TRIGGERS:
raise ValueError(f"Unknown Z-Wave JS trigger platform {config[CONF_PLATFORM]}")
return TRIGGERS[platform_split[1]]
async def async_validate_trigger_config(
hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
platform = _get_trigger_platform(config)
return await platform.async_validate_trigger_config(hass, config)
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach trigger of specified platform."""
platform = _get_trigger_platform(config)
return await platform.async_attach_trigger(hass, config, action, trigger_info)
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for Z-Wave JS."""
return TRIGGERS
@@ -16,7 +16,7 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from ..const import (
@@ -251,3 +251,29 @@ async def async_attach_trigger(
_create_zwave_listeners()
return async_remove
class EventTrigger(Trigger):
"""Z-Wave JS event trigger."""
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize trigger."""
self._config = config
self._hass = hass
@classmethod
async def async_validate_trigger_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return await async_validate_trigger_config(hass, config)
async def async_attach_trigger(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
return await async_attach_trigger(
self._hass, self._config, action, trigger_info
)
@@ -14,7 +14,7 @@ from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM, M
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from ..config_validation import VALUE_SCHEMA
@@ -202,3 +202,29 @@ async def async_attach_trigger(
_create_zwave_listeners()
return async_remove
class ValueUpdatedTrigger(Trigger):
"""Z-Wave JS value updated trigger."""
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
"""Initialize trigger."""
self._config = config
self._hass = hass
@classmethod
async def async_validate_trigger_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return await async_validate_trigger_config(hass, config)
async def async_attach_trigger(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
return await async_attach_trigger(
self._hass, self._config, action, trigger_info
)
-432
View File
@@ -26,438 +26,6 @@ DHCP: Final[list[dict[str, str | bool]]] = [
"domain": "airzone",
"macaddress": "E84F25*",
},
{
"domain": "amazon_devices",
"macaddress": "007147*",
},
{
"domain": "amazon_devices",
"macaddress": "00FC8B*",
},
{
"domain": "amazon_devices",
"macaddress": "0812A5*",
},
{
"domain": "amazon_devices",
"macaddress": "086AE5*",
},
{
"domain": "amazon_devices",
"macaddress": "08849D*",
},
{
"domain": "amazon_devices",
"macaddress": "089115*",
},
{
"domain": "amazon_devices",
"macaddress": "08A6BC*",
},
{
"domain": "amazon_devices",
"macaddress": "08C224*",
},
{
"domain": "amazon_devices",
"macaddress": "0CDC91*",
},
{
"domain": "amazon_devices",
"macaddress": "0CEE99*",
},
{
"domain": "amazon_devices",
"macaddress": "1009F9*",
},
{
"domain": "amazon_devices",
"macaddress": "109693*",
},
{
"domain": "amazon_devices",
"macaddress": "10BF67*",
},
{
"domain": "amazon_devices",
"macaddress": "10CE02*",
},
{
"domain": "amazon_devices",
"macaddress": "140AC5*",
},
{
"domain": "amazon_devices",
"macaddress": "149138*",
},
{
"domain": "amazon_devices",
"macaddress": "1848BE*",
},
{
"domain": "amazon_devices",
"macaddress": "1C12B0*",
},
{
"domain": "amazon_devices",
"macaddress": "1C4D66*",
},
{
"domain": "amazon_devices",
"macaddress": "1C93C4*",
},
{
"domain": "amazon_devices",
"macaddress": "1CFE2B*",
},
{
"domain": "amazon_devices",
"macaddress": "244CE3*",
},
{
"domain": "amazon_devices",
"macaddress": "24CE33*",
},
{
"domain": "amazon_devices",
"macaddress": "2873F6*",
},
{
"domain": "amazon_devices",
"macaddress": "2C71FF*",
},
{
"domain": "amazon_devices",
"macaddress": "34AFB3*",
},
{
"domain": "amazon_devices",
"macaddress": "34D270*",
},
{
"domain": "amazon_devices",
"macaddress": "38F73D*",
},
{
"domain": "amazon_devices",
"macaddress": "3C5CC4*",
},
{
"domain": "amazon_devices",
"macaddress": "3CE441*",
},
{
"domain": "amazon_devices",
"macaddress": "440049*",
},
{
"domain": "amazon_devices",
"macaddress": "40A2DB*",
},
{
"domain": "amazon_devices",
"macaddress": "40A9CF*",
},
{
"domain": "amazon_devices",
"macaddress": "40B4CD*",
},
{
"domain": "amazon_devices",
"macaddress": "443D54*",
},
{
"domain": "amazon_devices",
"macaddress": "44650D*",
},
{
"domain": "amazon_devices",
"macaddress": "485F2D*",
},
{
"domain": "amazon_devices",
"macaddress": "48785E*",
},
{
"domain": "amazon_devices",
"macaddress": "48B423*",
},
{
"domain": "amazon_devices",
"macaddress": "4C1744*",
},
{
"domain": "amazon_devices",
"macaddress": "4CEFC0*",
},
{
"domain": "amazon_devices",
"macaddress": "5007C3*",
},
{
"domain": "amazon_devices",
"macaddress": "50D45C*",
},
{
"domain": "amazon_devices",
"macaddress": "50DCE7*",
},
{
"domain": "amazon_devices",
"macaddress": "50F5DA*",
},
{
"domain": "amazon_devices",
"macaddress": "5C415A*",
},
{
"domain": "amazon_devices",
"macaddress": "6837E9*",
},
{
"domain": "amazon_devices",
"macaddress": "6854FD*",
},
{
"domain": "amazon_devices",
"macaddress": "689A87*",
},
{
"domain": "amazon_devices",
"macaddress": "68B691*",
},
{
"domain": "amazon_devices",
"macaddress": "68DBF5*",
},
{
"domain": "amazon_devices",
"macaddress": "68F63B*",
},
{
"domain": "amazon_devices",
"macaddress": "6C0C9A*",
},
{
"domain": "amazon_devices",
"macaddress": "6C5697*",
},
{
"domain": "amazon_devices",
"macaddress": "7458F3*",
},
{
"domain": "amazon_devices",
"macaddress": "74C246*",
},
{
"domain": "amazon_devices",
"macaddress": "74D637*",
},
{
"domain": "amazon_devices",
"macaddress": "74E20C*",
},
{
"domain": "amazon_devices",
"macaddress": "74ECB2*",
},
{
"domain": "amazon_devices",
"macaddress": "786C84*",
},
{
"domain": "amazon_devices",
"macaddress": "78A03F*",
},
{
"domain": "amazon_devices",
"macaddress": "7C6166*",
},
{
"domain": "amazon_devices",
"macaddress": "7C6305*",
},
{
"domain": "amazon_devices",
"macaddress": "7CD566*",
},
{
"domain": "amazon_devices",
"macaddress": "8871E5*",
},
{
"domain": "amazon_devices",
"macaddress": "901195*",
},
{
"domain": "amazon_devices",
"macaddress": "90235B*",
},
{
"domain": "amazon_devices",
"macaddress": "90A822*",
},
{
"domain": "amazon_devices",
"macaddress": "90F82E*",
},
{
"domain": "amazon_devices",
"macaddress": "943A91*",
},
{
"domain": "amazon_devices",
"macaddress": "98226E*",
},
{
"domain": "amazon_devices",
"macaddress": "98CCF3*",
},
{
"domain": "amazon_devices",
"macaddress": "9CC8E9*",
},
{
"domain": "amazon_devices",
"macaddress": "A002DC*",
},
{
"domain": "amazon_devices",
"macaddress": "A0D2B1*",
},
{
"domain": "amazon_devices",
"macaddress": "A40801*",
},
{
"domain": "amazon_devices",
"macaddress": "A8E621*",
},
{
"domain": "amazon_devices",
"macaddress": "AC416A*",
},
{
"domain": "amazon_devices",
"macaddress": "AC63BE*",
},
{
"domain": "amazon_devices",
"macaddress": "ACCCFC*",
},
{
"domain": "amazon_devices",
"macaddress": "B0739C*",
},
{
"domain": "amazon_devices",
"macaddress": "B0CFCB*",
},
{
"domain": "amazon_devices",
"macaddress": "B0F7C4*",
},
{
"domain": "amazon_devices",
"macaddress": "B85F98*",
},
{
"domain": "amazon_devices",
"macaddress": "C091B9*",
},
{
"domain": "amazon_devices",
"macaddress": "C095CF*",
},
{
"domain": "amazon_devices",
"macaddress": "C49500*",
},
{
"domain": "amazon_devices",
"macaddress": "C86C3D*",
},
{
"domain": "amazon_devices",
"macaddress": "CC9EA2*",
},
{
"domain": "amazon_devices",
"macaddress": "CCF735*",
},
{
"domain": "amazon_devices",
"macaddress": "DC54D7*",
},
{
"domain": "amazon_devices",
"macaddress": "D8BE65*",
},
{
"domain": "amazon_devices",
"macaddress": "D8FBD6*",
},
{
"domain": "amazon_devices",
"macaddress": "DC91BF*",
},
{
"domain": "amazon_devices",
"macaddress": "DCA0D0*",
},
{
"domain": "amazon_devices",
"macaddress": "E0F728*",
},
{
"domain": "amazon_devices",
"macaddress": "EC2BEB*",
},
{
"domain": "amazon_devices",
"macaddress": "EC8AC4*",
},
{
"domain": "amazon_devices",
"macaddress": "ECA138*",
},
{
"domain": "amazon_devices",
"macaddress": "F02F9E*",
},
{
"domain": "amazon_devices",
"macaddress": "F0272D*",
},
{
"domain": "amazon_devices",
"macaddress": "F0F0A4*",
},
{
"domain": "amazon_devices",
"macaddress": "F4032A*",
},
{
"domain": "amazon_devices",
"macaddress": "F854B8*",
},
{
"domain": "amazon_devices",
"macaddress": "FC492D*",
},
{
"domain": "amazon_devices",
"macaddress": "FC65DE*",
},
{
"domain": "amazon_devices",
"macaddress": "FCA183*",
},
{
"domain": "amazon_devices",
"macaddress": "FCE9D8*",
},
{
"domain": "august",
"hostname": "connect",
+4 -2
View File
@@ -62,7 +62,7 @@ def async_device_info_to_link_from_device_id(
def async_remove_stale_devices_links_keep_entity_device(
hass: HomeAssistant,
entry_id: str,
source_entity_id_or_uuid: str,
source_entity_id_or_uuid: str | None,
) -> None:
"""Remove entry_id from all devices except that of source_entity_id_or_uuid.
@@ -73,7 +73,9 @@ def async_remove_stale_devices_links_keep_entity_device(
async_remove_stale_devices_links_keep_current_device(
hass=hass,
entry_id=entry_id,
current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid),
current_device_id=async_entity_id_to_device_id(hass, source_entity_id_or_uuid)
if source_entity_id_or_uuid
else None,
)
+46 -1
View File
@@ -56,7 +56,7 @@ EVENT_DEVICE_REGISTRY_UPDATED: EventType[EventDeviceRegistryUpdatedData] = Event
)
STORAGE_KEY = "core.device_registry"
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 9
STORAGE_VERSION_MINOR = 10
CLEANUP_DELAY = 10
@@ -394,13 +394,17 @@ class DeviceEntry:
class DeletedDeviceEntry:
"""Deleted Device Registry Entry."""
area_id: str | None = attr.ib()
config_entries: set[str] = attr.ib()
config_entries_subentries: dict[str, set[str | None]] = attr.ib()
connections: set[tuple[str, str]] = attr.ib()
created_at: datetime = attr.ib()
disabled_by: DeviceEntryDisabler | None = attr.ib()
id: str = attr.ib()
identifiers: set[tuple[str, str]] = attr.ib()
labels: set[str] = attr.ib()
modified_at: datetime = attr.ib()
name_by_user: str | None = attr.ib()
orphaned_timestamp: float | None = attr.ib()
_cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
@@ -413,14 +417,18 @@ class DeletedDeviceEntry:
) -> DeviceEntry:
"""Create DeviceEntry from DeletedDeviceEntry."""
return DeviceEntry(
area_id=self.area_id,
# type ignores: likely https://github.com/python/mypy/issues/8625
config_entries={config_entry_id}, # type: ignore[arg-type]
config_entries_subentries={config_entry_id: {config_subentry_id}},
connections=self.connections & connections, # type: ignore[arg-type]
created_at=self.created_at,
disabled_by=self.disabled_by,
identifiers=self.identifiers & identifiers, # type: ignore[arg-type]
id=self.id,
is_new=True,
labels=self.labels, # type: ignore[arg-type]
name_by_user=self.name_by_user,
)
@under_cached_property
@@ -429,6 +437,7 @@ class DeletedDeviceEntry:
return json_fragment(
json_bytes(
{
"area_id": self.area_id,
# The config_entries list can be removed from the storage
# representation in HA Core 2026.2
"config_entries": list(self.config_entries),
@@ -438,9 +447,12 @@ class DeletedDeviceEntry:
},
"connections": list(self.connections),
"created_at": self.created_at,
"disabled_by": self.disabled_by,
"identifiers": list(self.identifiers),
"id": self.id,
"labels": list(self.labels),
"modified_at": self.modified_at,
"name_by_user": self.name_by_user,
"orphaned_timestamp": self.orphaned_timestamp,
}
)
@@ -540,6 +552,13 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
config_entry_id: {None}
for config_entry_id in device["config_entries"]
}
if old_minor_version < 10:
# Introduced in 2025.6
for device in old_data["deleted_devices"]:
device["area_id"] = None
device["disabled_by"] = None
device["labels"] = []
device["name_by_user"] = None
if old_major_version > 2:
raise NotImplementedError
@@ -1238,13 +1257,17 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self.hass.verify_event_loop_thread("device_registry.async_remove_device")
device = self.devices.pop(device_id)
self.deleted_devices[device_id] = DeletedDeviceEntry(
area_id=device.area_id,
config_entries=device.config_entries,
config_entries_subentries=device.config_entries_subentries,
connections=device.connections,
created_at=device.created_at,
disabled_by=device.disabled_by,
identifiers=device.identifiers,
id=device.id,
labels=device.labels,
modified_at=utcnow(),
name_by_user=device.name_by_user,
orphaned_timestamp=None,
)
for other_device in list(self.devices.values()):
@@ -1316,6 +1339,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# Introduced in 0.111
for device in data["deleted_devices"]:
deleted_devices[device["id"]] = DeletedDeviceEntry(
area_id=device["area_id"],
config_entries=set(device["config_entries"]),
config_entries_subentries={
config_entry_id: set(subentries)
@@ -1325,9 +1349,16 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
},
connections={tuple(conn) for conn in device["connections"]},
created_at=datetime.fromisoformat(device["created_at"]),
disabled_by=(
DeviceEntryDisabler(device["disabled_by"])
if device["disabled_by"]
else None
),
identifiers={tuple(iden) for iden in device["identifiers"]},
id=device["id"],
labels=set(device["labels"]),
modified_at=datetime.fromisoformat(device["modified_at"]),
name_by_user=device["name_by_user"],
orphaned_timestamp=device["orphaned_timestamp"],
)
@@ -1448,12 +1479,26 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
"""Clear area id from registry entries."""
for device in self.devices.get_devices_for_area_id(area_id):
self.async_update_device(device.id, area_id=None)
for deleted_device in list(self.deleted_devices.values()):
if deleted_device.area_id != area_id:
continue
self.deleted_devices[deleted_device.id] = attr.evolve(
deleted_device, area_id=None
)
self.async_schedule_save()
@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for device in self.devices.get_devices_for_label(label_id):
self.async_update_device(device.id, labels=device.labels - {label_id})
for deleted_device in list(self.deleted_devices.values()):
if label_id not in deleted_device.labels:
continue
self.deleted_devices[deleted_device.id] = attr.evolve(
deleted_device, labels=deleted_device.labels - {label_id}
)
self.async_schedule_save()
@callback
+5 -1
View File
@@ -92,7 +92,11 @@ def async_setup(hass: HomeAssistant) -> None:
@bind_hass
@singleton.singleton(DATA_ENTITY_SOURCE)
def entity_sources(hass: HomeAssistant) -> dict[str, EntityInfo]:
"""Get the entity sources."""
"""Get the entity sources.
Items are added to this dict by Entity.async_internal_added_to_hass and
removed by Entity.async_internal_will_remove_from_hass.
"""
return {}
+125 -10
View File
@@ -79,7 +79,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 17
STORAGE_VERSION_MINOR = 18
STORAGE_KEY = "core.entity_registry"
CLEANUP_INTERVAL = 3600 * 24
@@ -406,12 +406,23 @@ class DeletedRegistryEntry:
entity_id: str = attr.ib()
unique_id: str = attr.ib()
platform: str = attr.ib()
aliases: set[str] = attr.ib()
area_id: str | None = attr.ib()
categories: dict[str, str] = attr.ib()
config_entry_id: str | None = attr.ib()
config_subentry_id: str | None = attr.ib()
created_at: datetime = attr.ib()
device_class: str | None = attr.ib()
disabled_by: RegistryEntryDisabler | None = attr.ib()
domain: str = attr.ib(init=False, repr=False)
hidden_by: RegistryEntryHider | None = attr.ib()
icon: str | None = attr.ib()
id: str = attr.ib()
labels: set[str] = attr.ib()
modified_at: datetime = attr.ib()
name: str | None = attr.ib()
options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options)
orphaned_timestamp: float | None = attr.ib()
_cache: dict[str, Any] = attr.ib(factory=dict, eq=False, init=False)
@@ -427,12 +438,22 @@ class DeletedRegistryEntry:
return json_fragment(
json_bytes(
{
"aliases": list(self.aliases),
"area_id": self.area_id,
"categories": self.categories,
"config_entry_id": self.config_entry_id,
"config_subentry_id": self.config_subentry_id,
"created_at": self.created_at,
"device_class": self.device_class,
"disabled_by": self.disabled_by,
"entity_id": self.entity_id,
"hidden_by": self.hidden_by,
"icon": self.icon,
"id": self.id,
"labels": list(self.labels),
"modified_at": self.modified_at,
"name": self.name,
"options": self.options,
"orphaned_timestamp": self.orphaned_timestamp,
"platform": self.platform,
"unique_id": self.unique_id,
@@ -556,6 +577,20 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
for entity in data["entities"]:
entity["suggested_object_id"] = None
if old_minor_version < 18:
# Version 1.18 adds user customizations to deleted entities
for entity in data["deleted_entities"]:
entity["aliases"] = []
entity["area_id"] = None
entity["categories"] = {}
entity["device_class"] = None
entity["disabled_by"] = None
entity["hidden_by"] = None
entity["icon"] = None
entity["labels"] = []
entity["name"] = None
entity["options"] = {}
if old_major_version > 1:
raise NotImplementedError
return data
@@ -916,15 +951,40 @@ class EntityRegistry(BaseRegistry):
entity_registry_id: str | None = None
created_at = utcnow()
deleted_entity = self.deleted_entities.pop((domain, platform, unique_id), None)
options: Mapping[str, Mapping[str, Any]] | None
if deleted_entity is not None:
# Restore id
entity_registry_id = deleted_entity.id
aliases = deleted_entity.aliases
area_id = deleted_entity.area_id
categories = deleted_entity.categories
created_at = deleted_entity.created_at
device_class = deleted_entity.device_class
disabled_by = deleted_entity.disabled_by
# Restore entity_id if it's available
if self._entity_id_available(deleted_entity.entity_id):
entity_id = deleted_entity.entity_id
entity_registry_id = deleted_entity.id
hidden_by = deleted_entity.hidden_by
icon = deleted_entity.icon
labels = deleted_entity.labels
name = deleted_entity.name
options = deleted_entity.options
else:
aliases = set()
area_id = None
categories = {}
device_class = None
icon = None
labels = set()
name = None
options = get_initial_options() if get_initial_options else None
entity_id = self.async_generate_entity_id(
domain,
suggested_object_id or calculated_object_id or f"{platform}_{unique_id}",
)
if not entity_id:
entity_id = self.async_generate_entity_id(
domain,
suggested_object_id
or calculated_object_id
or f"{platform}_{unique_id}",
)
if (
disabled_by is None
@@ -938,21 +998,26 @@ class EntityRegistry(BaseRegistry):
"""Return None if value is UNDEFINED, otherwise return value."""
return None if value is UNDEFINED else value
initial_options = get_initial_options() if get_initial_options else None
entry = RegistryEntry(
aliases=aliases,
area_id=area_id,
categories=categories,
capabilities=none_if_undefined(capabilities),
config_entry_id=none_if_undefined(config_entry_id),
config_subentry_id=none_if_undefined(config_subentry_id),
created_at=created_at,
device_class=device_class,
device_id=none_if_undefined(device_id),
disabled_by=disabled_by,
entity_category=none_if_undefined(entity_category),
entity_id=entity_id,
hidden_by=hidden_by,
has_entity_name=none_if_undefined(has_entity_name) or False,
icon=icon,
id=entity_registry_id,
options=initial_options,
labels=labels,
name=name,
options=options,
original_device_class=none_if_undefined(original_device_class),
original_icon=none_if_undefined(original_icon),
original_name=none_if_undefined(original_name),
@@ -986,12 +1051,22 @@ class EntityRegistry(BaseRegistry):
# If the entity does not belong to a config entry, mark it as orphaned
orphaned_timestamp = None if config_entry_id else time.time()
self.deleted_entities[key] = DeletedRegistryEntry(
aliases=entity.aliases,
area_id=entity.area_id,
categories=entity.categories,
config_entry_id=config_entry_id,
config_subentry_id=entity.config_subentry_id,
created_at=entity.created_at,
device_class=entity.device_class,
disabled_by=entity.disabled_by,
entity_id=entity_id,
hidden_by=entity.hidden_by,
icon=entity.icon,
id=entity.id,
labels=entity.labels,
modified_at=utcnow(),
name=entity.name,
options=entity.options,
orphaned_timestamp=orphaned_timestamp,
platform=entity.platform,
unique_id=entity.unique_id,
@@ -1420,12 +1495,30 @@ class EntityRegistry(BaseRegistry):
entity["unique_id"],
)
deleted_entities[key] = DeletedRegistryEntry(
aliases=set(entity["aliases"]),
area_id=entity["area_id"],
categories=entity["categories"],
config_entry_id=entity["config_entry_id"],
config_subentry_id=entity["config_subentry_id"],
created_at=datetime.fromisoformat(entity["created_at"]),
device_class=entity["device_class"],
disabled_by=(
RegistryEntryDisabler(entity["disabled_by"])
if entity["disabled_by"]
else None
),
entity_id=entity["entity_id"],
hidden_by=(
RegistryEntryHider(entity["hidden_by"])
if entity["hidden_by"]
else None
),
icon=entity["icon"],
id=entity["id"],
labels=set(entity["labels"]),
modified_at=datetime.fromisoformat(entity["modified_at"]),
name=entity["name"],
options=entity["options"],
orphaned_timestamp=entity["orphaned_timestamp"],
platform=entity["platform"],
unique_id=entity["unique_id"],
@@ -1455,12 +1548,29 @@ class EntityRegistry(BaseRegistry):
categories = entry.categories.copy()
del categories[scope]
self.async_update_entity(entity_id, categories=categories)
for key, deleted_entity in list(self.deleted_entities.items()):
if (
existing_category_id := deleted_entity.categories.get(scope)
) and category_id == existing_category_id:
categories = deleted_entity.categories.copy()
del categories[scope]
self.deleted_entities[key] = attr.evolve(
deleted_entity, categories=categories
)
self.async_schedule_save()
@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for entry in self.entities.get_entries_for_label(label_id):
self.async_update_entity(entry.entity_id, labels=entry.labels - {label_id})
for key, deleted_entity in list(self.deleted_entities.items()):
if label_id not in deleted_entity.labels:
continue
self.deleted_entities[key] = attr.evolve(
deleted_entity, labels=deleted_entity.labels - {label_id}
)
self.async_schedule_save()
@callback
def async_clear_config_entry(self, config_entry_id: str) -> None:
@@ -1525,6 +1635,11 @@ class EntityRegistry(BaseRegistry):
"""Clear area id from registry entries."""
for entry in self.entities.get_entries_for_area_id(area_id):
self.async_update_entity(entry.entity_id, area_id=None)
for key, deleted_entity in list(self.deleted_entities.items()):
if deleted_entity.area_id != area_id:
continue
self.deleted_entities[key] = attr.evolve(deleted_entity, area_id=None)
self.async_schedule_save()
@callback

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