This commit is contained in:
Bram Kragten 2025-07-22 10:30:09 +02:00 committed by GitHub
commit 456f992b7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 284 additions and 75 deletions

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioamazondevices"], "loggers": ["aioamazondevices"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["aioamazondevices==3.2.10"] "requirements": ["aioamazondevices==3.5.0"]
} }

View File

@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["async_upnp_client"], "loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], "requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"],
"ssdp": [ "ssdp": [
{ {
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@ -7,7 +7,7 @@
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms", "documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["async-upnp-client==0.44.0"], "requirements": ["async-upnp-client==0.45.0"],
"ssdp": [ "ssdp": [
{ {
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1", "deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

View File

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

View File

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

View File

@ -7,5 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["dacite", "gios"], "loggers": ["dacite", "gios"],
"requirements": ["gios==6.1.0"] "requirements": ["gios==6.1.2"]
} }

View File

@ -306,6 +306,11 @@ class WebRTCProvider(CameraWebRTCProvider):
await self.teardown() await self.teardown()
raise HomeAssistantError("Camera has no stream source") raise HomeAssistantError("Camera has no stream source")
if camera.platform.platform_name == "generic":
# This is a workaround to use ffmpeg for generic cameras
# A proper fix will be added in the future together with supporting multiple streams per camera
stream_source = "ffmpeg:" + stream_source
if not self.async_is_supported(stream_source): if not self.async_is_supported(stream_source):
await self.teardown() await self.teardown()
raise HomeAssistantError("Stream source is not supported by go2rtc") raise HomeAssistantError("Stream source is not supported by go2rtc")

View File

@ -113,9 +113,7 @@ class HomematicipHAP:
self._ws_close_requested = False self._ws_close_requested = False
self._ws_connection_closed = asyncio.Event() self._ws_connection_closed = asyncio.Event()
self._retry_task: asyncio.Task | None = None self._get_state_task: asyncio.Task | None = None
self._tries = 0
self._accesspoint_connected = True
self.hmip_device_by_entity_id: dict[str, Any] = {} self.hmip_device_by_entity_id: dict[str, Any] = {}
self.reset_connection_listener: Callable | None = None self.reset_connection_listener: Callable | None = None
@ -161,17 +159,8 @@ class HomematicipHAP:
""" """
if not self.home.connected: if not self.home.connected:
_LOGGER.error("HMIP access point has lost connection with the cloud") _LOGGER.error("HMIP access point has lost connection with the cloud")
self._accesspoint_connected = False self._ws_connection_closed.set()
self.set_all_to_unavailable() self.set_all_to_unavailable()
elif not self._accesspoint_connected:
# Now the HOME_CHANGED event has fired indicating the access
# point has reconnected to the cloud again.
# Explicitly getting an update as entity states might have
# changed during access point disconnect."""
job = self.hass.async_create_task(self.get_state())
job.add_done_callback(self.get_state_finished)
self._accesspoint_connected = True
@callback @callback
def async_create_entity(self, *args, **kwargs) -> None: def async_create_entity(self, *args, **kwargs) -> None:
@ -185,20 +174,43 @@ class HomematicipHAP:
await asyncio.sleep(30) await asyncio.sleep(30)
await self.hass.config_entries.async_reload(self.config_entry.entry_id) await self.hass.config_entries.async_reload(self.config_entry.entry_id)
async def _try_get_state(self) -> None:
"""Call get_state in a loop until no error occurs, using exponential backoff on error."""
# Wait until WebSocket connection is established.
while not self.home.websocket_is_connected():
await asyncio.sleep(2)
delay = 8
max_delay = 1500
while True:
try:
await self.get_state()
break
except HmipConnectionError as err:
_LOGGER.warning(
"Get_state failed, retrying in %s seconds: %s", delay, err
)
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay)
async def get_state(self) -> None: async def get_state(self) -> None:
"""Update HMIP state and tell Home Assistant.""" """Update HMIP state and tell Home Assistant."""
await self.home.get_current_state_async() await self.home.get_current_state_async()
self.update_all() self.update_all()
def get_state_finished(self, future) -> None: def get_state_finished(self, future) -> None:
"""Execute when get_state coroutine has finished.""" """Execute when try_get_state coroutine has finished."""
try: try:
future.result() future.result()
except HmipConnectionError: except Exception as err: # noqa: BLE001
# Somehow connection could not recover. Will disconnect and _LOGGER.error(
# so reconnect loop is taking over. "Error updating state after HMIP access point reconnect: %s", err
_LOGGER.error("Updating state after HMIP access point reconnect failed") )
self.hass.async_create_task(self.home.disable_events()) else:
_LOGGER.info(
"Updating state after HMIP access point reconnect finished successfully",
)
def set_all_to_unavailable(self) -> None: def set_all_to_unavailable(self) -> None:
"""Set all devices to unavailable and tell Home Assistant.""" """Set all devices to unavailable and tell Home Assistant."""
@ -222,8 +234,8 @@ class HomematicipHAP:
async def async_reset(self) -> bool: async def async_reset(self) -> bool:
"""Close the websocket connection.""" """Close the websocket connection."""
self._ws_close_requested = True self._ws_close_requested = True
if self._retry_task is not None: if self._get_state_task is not None:
self._retry_task.cancel() self._get_state_task.cancel()
await self.home.disable_events_async() await self.home.disable_events_async()
_LOGGER.debug("Closed connection to HomematicIP cloud server") _LOGGER.debug("Closed connection to HomematicIP cloud server")
await self.hass.config_entries.async_unload_platforms( await self.hass.config_entries.async_unload_platforms(
@ -247,7 +259,9 @@ class HomematicipHAP:
"""Handle websocket connected.""" """Handle websocket connected."""
_LOGGER.info("Websocket connection to HomematicIP Cloud established") _LOGGER.info("Websocket connection to HomematicIP Cloud established")
if self._ws_connection_closed.is_set(): if self._ws_connection_closed.is_set():
await self.get_state() self._get_state_task = self.hass.async_create_task(self._try_get_state())
self._get_state_task.add_done_callback(self.get_state_finished)
self._ws_connection_closed.clear() self._ws_connection_closed.clear()
async def ws_disconnected_handler(self) -> None: async def ws_disconnected_handler(self) -> None:
@ -256,11 +270,12 @@ class HomematicipHAP:
self._ws_connection_closed.set() self._ws_connection_closed.set()
async def ws_reconnected_handler(self, reason: str) -> None: async def ws_reconnected_handler(self, reason: str) -> None:
"""Handle websocket reconnection.""" """Handle websocket reconnection. Is called when Websocket tries to reconnect."""
_LOGGER.info( _LOGGER.info(
"Websocket connection to HomematicIP Cloud re-established due to reason: %s", "Websocket connection to HomematicIP Cloud trying to reconnect due to reason: %s",
reason, reason,
) )
self._ws_connection_closed.set() self._ws_connection_closed.set()
async def get_hap( async def get_hap(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["homematicip"], "loggers": ["homematicip"],
"requirements": ["homematicip==2.0.6"] "requirements": ["homematicip==2.0.7"]
} }

View File

@ -98,6 +98,12 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
f"together with state class `{state_class}`" f"together with state class `{state_class}`"
) )
unit_of_measurement: str | None
if (
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
) is not None and not unit_of_measurement.strip():
config.pop(CONF_UNIT_OF_MEASUREMENT)
# Only allow `options` to be set for `enum` sensors # Only allow `options` to be set for `enum` sensors
# to limit the possible sensor values # to limit the possible sensor values
if (options := config.get(CONF_OPTIONS)) is not None: if (options := config.get(CONF_OPTIONS)) is not None:

View File

@ -40,7 +40,7 @@
"samsungctl[websocket]==0.7.1", "samsungctl[websocket]==0.7.1",
"samsungtvws[async,encrypted]==2.7.2", "samsungtvws[async,encrypted]==2.7.2",
"wakeonlan==3.1.0", "wakeonlan==3.1.0",
"async-upnp-client==0.44.0" "async-upnp-client==0.45.0"
], ],
"ssdp": [ "ssdp": [
{ {

View File

@ -868,8 +868,8 @@ RPC_SENSORS: Final = {
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT, device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
available=lambda status: (status and status["n_current"]) is not None, removal_condition=lambda _config, status, key: status[key].get("n_current")
removal_condition=lambda _config, status, _key: "n_current" not in status, is None,
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
), ),
"total_current": RpcSensorDescription( "total_current": RpcSensorDescription(

View File

@ -30,5 +30,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pysmartthings"], "loggers": ["pysmartthings"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pysmartthings==3.2.7"] "requirements": ["pysmartthings==3.2.8"]
} }

View File

@ -72,7 +72,7 @@ class SonosFavorites(SonosHouseholdCoordinator):
"""Process the event payload in an async lock and update entities.""" """Process the event payload in an async lock and update entities."""
event_id = event.variables["favorites_update_id"] event_id = event.variables["favorites_update_id"]
container_ids = event.variables["container_update_i_ds"] container_ids = event.variables["container_update_i_ds"]
if not (match := re.search(r"FV:2,(\d+)", container_ids)): if not container_ids or not (match := re.search(r"FV:2,(\d+)", container_ids)):
return return
container_id = int(match.groups()[0]) container_id = int(match.groups()[0])

View File

@ -8,5 +8,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["async_upnp_client"], "loggers": ["async_upnp_client"],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["async-upnp-client==0.44.0"] "requirements": ["async-upnp-client==0.45.0"]
} }

View File

@ -41,5 +41,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["switchbot"], "loggers": ["switchbot"],
"quality_scale": "gold", "quality_scale": "gold",
"requirements": ["PySwitchbot==0.68.1"] "requirements": ["PySwitchbot==0.68.2"]
} }

View File

@ -28,6 +28,7 @@ class SyncthruCoordinator(DataUpdateCoordinator[SyncThru]):
hass, hass,
_LOGGER, _LOGGER,
name=DOMAIN, name=DOMAIN,
config_entry=entry,
update_interval=timedelta(seconds=30), update_interval=timedelta(seconds=30),
) )
self.syncthru = SyncThru( self.syncthru = SyncThru(

View File

@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet", "documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.0"] "requirements": ["tesla-fleet-api==1.2.2"]
} }

View File

@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TeslemetryConfigEntry from . import TeslemetryConfigEntry
from .entity import TeslemetryVehiclePollingEntity from .entity import TeslemetryVehicleStreamEntity
from .helpers import handle_command, handle_vehicle_command from .helpers import handle_command, handle_vehicle_command
from .models import TeslemetryVehicleData from .models import TeslemetryVehicleData
@ -74,7 +74,7 @@ async def async_setup_entry(
) )
class TeslemetryButtonEntity(TeslemetryVehiclePollingEntity, ButtonEntity): class TeslemetryButtonEntity(TeslemetryVehicleStreamEntity, ButtonEntity):
"""Base class for Teslemetry buttons.""" """Base class for Teslemetry buttons."""
api: Vehicle api: Vehicle

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/teslemetry", "documentation": "https://www.home-assistant.io/integrations/teslemetry",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"], "loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.0", "teslemetry-stream==0.7.9"] "requirements": ["tesla-fleet-api==1.2.2", "teslemetry-stream==0.7.9"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie", "documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"], "loggers": ["tessie", "tesla-fleet-api"],
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.0"] "requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.2"]
} }

View File

@ -8,7 +8,7 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["async_upnp_client"], "loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], "requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"],
"ssdp": [ "ssdp": [
{ {
"st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1"

View File

@ -16,7 +16,7 @@
}, },
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["async_upnp_client", "yeelight"], "loggers": ["async_upnp_client", "yeelight"],
"requirements": ["yeelight==0.7.16", "async-upnp-client==0.44.0"], "requirements": ["yeelight==0.7.16", "async-upnp-client==0.45.0"],
"zeroconf": [ "zeroconf": [
{ {
"type": "_miio._udp.local.", "type": "_miio._udp.local.",

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025 MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 7 MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "2" PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@ -13,7 +13,7 @@ aiozoneinfo==0.2.3
annotatedyaml==0.4.5 annotatedyaml==0.4.5
astral==2.2 astral==2.2
async-interrupt==1.2.2 async-interrupt==1.2.2
async-upnp-client==0.44.0 async-upnp-client==0.45.0
atomicwrites-homeassistant==1.4.1 atomicwrites-homeassistant==1.4.1
attrs==25.3.0 attrs==25.3.0
audioop-lts==0.2.1 audioop-lts==0.2.1
@ -38,7 +38,7 @@ habluetooth==3.49.0
hass-nabucasa==0.106.0 hass-nabucasa==0.106.0
hassil==2.2.3 hassil==2.2.3
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250702.2 home-assistant-frontend==20250702.3
home-assistant-intents==2025.6.23 home-assistant-intents==2025.6.23
httpx==0.28.1 httpx==0.28.1
ifaddr==0.2.0 ifaddr==0.2.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2025.7.2" version = "2025.7.3"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"] license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."

18
requirements_all.txt generated
View File

@ -84,7 +84,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.68.1 PySwitchbot==0.68.2
# homeassistant.components.switchmate # homeassistant.components.switchmate
PySwitchmate==0.5.1 PySwitchmate==0.5.1
@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12
aioairzone==1.0.0 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==3.2.10 aioamazondevices==3.5.0
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -527,7 +527,7 @@ asmog==0.0.6
# homeassistant.components.ssdp # homeassistant.components.ssdp
# homeassistant.components.upnp # homeassistant.components.upnp
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.44.0 async-upnp-client==0.45.0
# homeassistant.components.arve # homeassistant.components.arve
asyncarve==0.1.1 asyncarve==0.1.1
@ -1020,7 +1020,7 @@ georss-qld-bushfire-alert-client==0.8
getmac==0.9.5 getmac==0.9.5
# homeassistant.components.gios # homeassistant.components.gios
gios==6.1.0 gios==6.1.2
# homeassistant.components.gitter # homeassistant.components.gitter
gitterpy==0.1.7 gitterpy==0.1.7
@ -1168,13 +1168,13 @@ hole==0.8.0
holidays==0.75 holidays==0.75
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250702.2 home-assistant-frontend==20250702.3
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.6.23 home-assistant-intents==2025.6.23
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==2.0.6 homematicip==2.0.7
# homeassistant.components.horizon # homeassistant.components.horizon
horimote==0.4.1 horimote==0.4.1
@ -1962,7 +1962,7 @@ pyeiscp==0.0.7
pyemoncms==0.1.1 pyemoncms==0.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==2.2.1 pyenphase==2.2.2
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==4.7 pyenvisalink==4.7
@ -2348,7 +2348,7 @@ pysmappee==0.2.29
pysmarlaapi==0.9.0 pysmarlaapi==0.9.0
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.7 pysmartthings==3.2.8
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2904,7 +2904,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet # homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie # homeassistant.components.tessie
tesla-fleet-api==1.2.0 tesla-fleet-api==1.2.2
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.5.2 tesla-powerwall==0.5.2

View File

@ -81,7 +81,7 @@ PyQRCode==1.2.1
PyRMVtransport==0.3.3 PyRMVtransport==0.3.3
# homeassistant.components.switchbot # homeassistant.components.switchbot
PySwitchbot==0.68.1 PySwitchbot==0.68.2
# homeassistant.components.syncthru # homeassistant.components.syncthru
PySyncThru==0.8.0 PySyncThru==0.8.0
@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12
aioairzone==1.0.0 aioairzone==1.0.0
# homeassistant.components.alexa_devices # homeassistant.components.alexa_devices
aioamazondevices==3.2.10 aioamazondevices==3.5.0
# homeassistant.components.ambient_network # homeassistant.components.ambient_network
# homeassistant.components.ambient_station # homeassistant.components.ambient_station
@ -491,7 +491,7 @@ arcam-fmj==1.8.1
# homeassistant.components.ssdp # homeassistant.components.ssdp
# homeassistant.components.upnp # homeassistant.components.upnp
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.44.0 async-upnp-client==0.45.0
# homeassistant.components.arve # homeassistant.components.arve
asyncarve==0.1.1 asyncarve==0.1.1
@ -890,7 +890,7 @@ georss-qld-bushfire-alert-client==0.8
getmac==0.9.5 getmac==0.9.5
# homeassistant.components.gios # homeassistant.components.gios
gios==6.1.0 gios==6.1.2
# homeassistant.components.glances # homeassistant.components.glances
glances-api==0.8.0 glances-api==0.8.0
@ -1017,13 +1017,13 @@ hole==0.8.0
holidays==0.75 holidays==0.75
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20250702.2 home-assistant-frontend==20250702.3
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2025.6.23 home-assistant-intents==2025.6.23
# homeassistant.components.homematicip_cloud # homeassistant.components.homematicip_cloud
homematicip==2.0.6 homematicip==2.0.7
# homeassistant.components.remember_the_milk # homeassistant.components.remember_the_milk
httplib2==0.20.4 httplib2==0.20.4
@ -1637,7 +1637,7 @@ pyeiscp==0.0.7
pyemoncms==0.1.1 pyemoncms==0.1.1
# homeassistant.components.enphase_envoy # homeassistant.components.enphase_envoy
pyenphase==2.2.1 pyenphase==2.2.2
# homeassistant.components.everlights # homeassistant.components.everlights
pyeverlights==0.1.0 pyeverlights==0.1.0
@ -1951,7 +1951,7 @@ pysmappee==0.2.29
pysmarlaapi==0.9.0 pysmarlaapi==0.9.0
# homeassistant.components.smartthings # homeassistant.components.smartthings
pysmartthings==3.2.7 pysmartthings==3.2.8
# homeassistant.components.smarty # homeassistant.components.smarty
pysmarty2==0.10.2 pysmarty2==0.10.2
@ -2390,7 +2390,7 @@ temperusb==1.6.1
# homeassistant.components.tesla_fleet # homeassistant.components.tesla_fleet
# homeassistant.components.teslemetry # homeassistant.components.teslemetry
# homeassistant.components.tessie # homeassistant.components.tessie
tesla-fleet-api==1.2.0 tesla-fleet-api==1.2.2
# homeassistant.components.powerwall # homeassistant.components.powerwall
tesla-powerwall==0.5.2 tesla-powerwall==0.5.2

View File

@ -670,3 +670,32 @@ async def test_async_get_image(
HomeAssistantError, match="Stream source is not supported by go2rtc" HomeAssistantError, match="Stream source is not supported by go2rtc"
): ):
await async_get_image(hass, camera.entity_id) await async_get_image(hass, camera.entity_id)
@pytest.mark.usefixtures("init_integration")
async def test_generic_workaround(
hass: HomeAssistant,
init_test_integration: MockCamera,
rest_client: AsyncMock,
) -> None:
"""Test workaround for generic integration cameras."""
camera = init_test_integration
assert isinstance(camera._webrtc_provider, WebRTCProvider)
image_bytes = load_fixture_bytes("snapshot.jpg", DOMAIN)
rest_client.get_jpeg_snapshot.return_value = image_bytes
camera.set_stream_source("https://my_stream_url.m3u8")
with patch.object(camera.platform, "platform_name", "generic"):
image = await async_get_image(hass, camera.entity_id)
assert image.content == image_bytes
rest_client.streams.add.assert_called_once_with(
camera.entity_id,
[
"ffmpeg:https://my_stream_url.m3u8",
f"ffmpeg:{camera.entity_id}#audio=opus#query=log_level=debug",
f"ffmpeg:{camera.entity_id}#video=mjpeg",
],
)

View File

@ -195,9 +195,14 @@ async def test_hap_reconnected(
ha_state = hass.states.get(entity_id) ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_UNAVAILABLE assert ha_state.state == STATE_UNAVAILABLE
mock_hap._accesspoint_connected = False with patch(
await async_manipulate_test_data(hass, mock_hap.home, "connected", True) "homeassistant.components.homematicip_cloud.hap.AsyncHome.websocket_is_connected",
await hass.async_block_till_done() return_value=True,
):
await async_manipulate_test_data(hass, mock_hap.home, "connected", True)
await mock_hap.ws_connected_handler()
await hass.async_block_till_done()
ha_state = hass.states.get(entity_id) ha_state = hass.states.get(entity_id)
assert ha_state.state == STATE_ON assert ha_state.state == STATE_ON

View File

@ -1,6 +1,6 @@
"""Test HomematicIP Cloud accesspoint.""" """Test HomematicIP Cloud accesspoint."""
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from homematicip.auth import Auth from homematicip.auth import Auth
from homematicip.connection.connection_context import ConnectionContext from homematicip.connection.connection_context import ConnectionContext
@ -242,7 +242,14 @@ async def test_get_state_after_disconnect(
hap = HomematicipHAP(hass, hmip_config_entry) hap = HomematicipHAP(hass, hmip_config_entry)
assert hap assert hap
with patch.object(hap, "get_state") as mock_get_state: simple_mock_home = AsyncMock(spec=AsyncHome, autospec=True)
hap.home = simple_mock_home
hap.home.websocket_is_connected = Mock(side_effect=[False, True])
with (
patch("asyncio.sleep", new=AsyncMock()) as mock_sleep,
patch.object(hap, "get_state") as mock_get_state,
):
assert not hap._ws_connection_closed.is_set() assert not hap._ws_connection_closed.is_set()
await hap.ws_connected_handler() await hap.ws_connected_handler()
@ -250,8 +257,54 @@ async def test_get_state_after_disconnect(
await hap.ws_disconnected_handler() await hap.ws_disconnected_handler()
assert hap._ws_connection_closed.is_set() assert hap._ws_connection_closed.is_set()
await hap.ws_connected_handler() with patch(
mock_get_state.assert_called_once() "homeassistant.components.homematicip_cloud.hap.AsyncHome.websocket_is_connected",
return_value=True,
):
await hap.ws_connected_handler()
mock_get_state.assert_called_once()
assert not hap._ws_connection_closed.is_set()
hap.home.websocket_is_connected.assert_called()
mock_sleep.assert_awaited_with(2)
async def test_try_get_state_exponential_backoff() -> None:
"""Test _try_get_state waits for websocket connection."""
# Arrange: Create instance and mock home
hap = HomematicipHAP(MagicMock(), MagicMock())
hap.home = MagicMock()
hap.home.websocket_is_connected = Mock(return_value=True)
hap.get_state = AsyncMock(
side_effect=[HmipConnectionError, HmipConnectionError, True]
)
with patch("asyncio.sleep", new=AsyncMock()) as mock_sleep:
await hap._try_get_state()
assert mock_sleep.mock_calls[0].args[0] == 8
assert mock_sleep.mock_calls[1].args[0] == 16
assert hap.get_state.call_count == 3
async def test_try_get_state_handle_exception() -> None:
"""Test _try_get_state handles exceptions."""
# Arrange: Create instance and mock home
hap = HomematicipHAP(MagicMock(), MagicMock())
hap.home = MagicMock()
expected_exception = Exception("Connection error")
future = AsyncMock()
future.result = Mock(side_effect=expected_exception)
with patch("homeassistant.components.homematicip_cloud.hap._LOGGER") as mock_logger:
hap.get_state_finished(future)
mock_logger.error.assert_called_once_with(
"Error updating state after HMIP access point reconnect: %s", expected_exception
)
async def test_async_connect( async def test_async_connect(

View File

@ -924,6 +924,30 @@ async def test_invalid_unit_of_measurement(
"device_class": None, "device_class": None,
"unit_of_measurement": None, "unit_of_measurement": None,
}, },
{
"name": "Test 4",
"state_topic": "test-topic",
"device_class": "ph",
"unit_of_measurement": "",
},
{
"name": "Test 5",
"state_topic": "test-topic",
"device_class": "ph",
"unit_of_measurement": " ",
},
{
"name": "Test 6",
"state_topic": "test-topic",
"device_class": None,
"unit_of_measurement": "",
},
{
"name": "Test 7",
"state_topic": "test-topic",
"device_class": None,
"unit_of_measurement": " ",
},
] ]
} }
} }
@ -936,10 +960,25 @@ async def test_valid_device_class_and_uom(
await mqtt_mock_entry() await mqtt_mock_entry()
state = hass.states.get("sensor.test_1") state = hass.states.get("sensor.test_1")
assert state is not None
assert state.attributes["device_class"] == "temperature" assert state.attributes["device_class"] == "temperature"
state = hass.states.get("sensor.test_2") state = hass.states.get("sensor.test_2")
assert state is not None
assert "device_class" not in state.attributes assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_3") state = hass.states.get("sensor.test_3")
assert state is not None
assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_4")
assert state is not None
assert state.attributes["device_class"] == "ph"
state = hass.states.get("sensor.test_5")
assert state is not None
assert state.attributes["device_class"] == "ph"
state = hass.states.get("sensor.test_6")
assert state is not None
assert "device_class" not in state.attributes
state = hass.states.get("sensor.test_7")
assert state is not None
assert "device_class" not in state.attributes assert "device_class" not in state.attributes

View File

@ -151,7 +151,7 @@
"c_pf": 0.72, "c_pf": 0.72,
"c_voltage": 230.2, "c_voltage": 230.2,
"id": 0, "id": 0,
"n_current": null, "n_current": 3.124,
"total_act_power": 2413.825, "total_act_power": 2413.825,
"total_aprt_power": 2525.779, "total_aprt_power": 2525.779,
"total_current": 11.116, "total_current": 11.116,

View File

@ -4303,6 +4303,62 @@
'state': '230.2', 'state': '230.2',
}) })
# --- # ---
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_name_phase_n_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 2,
}),
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Phase N current',
'platform': 'shelly',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC-em:0-n_current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_phase_n_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': 'Test name Phase N current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.test_name_phase_n_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3.124',
})
# ---
# name: test_shelly_pro_3em[sensor.test_name_rssi-entry] # name: test_shelly_pro_3em[sensor.test_name_rssi-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({