diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py
index 18b8ed1cca5..cb5c160e7b3 100644
--- a/homeassistant/components/zha/update.py
+++ b/homeassistant/components/zha/update.py
@@ -36,6 +36,18 @@ from .helpers import (
_LOGGER = logging.getLogger(__name__)
+OTA_MESSAGE_BATTERY_POWERED = (
+ "Battery powered devices can sometimes take multiple hours to update and you may"
+ " need to wake the device for the update to begin."
+)
+
+ZHA_DOCS_NETWORK_RELIABILITY = "https://www.home-assistant.io/integrations/zha/#zigbee-interference-avoidance-and-network-rangecoverage-optimization"
+OTA_MESSAGE_RELIABILITY = (
+ "If you are having issues updating a specific device, make sure that you've"
+ f" eliminated [common environmental issues]({ZHA_DOCS_NETWORK_RELIABILITY}) that"
+ " could be affecting network reliability. OTA updates require a reliable network."
+)
+
async def async_setup_entry(
hass: HomeAssistant,
@@ -149,7 +161,21 @@ class ZHAFirmwareUpdateEntity(
This is suitable for a long changelog that does not fit in the release_summary
property. The returned string can contain markdown.
"""
- return self.entity_data.entity.release_notes
+
+ if self.entity_data.device_proxy.device.is_mains_powered:
+ header = (
+ ""
+ f"{OTA_MESSAGE_RELIABILITY}"
+ ""
+ )
+ else:
+ header = (
+ ""
+ f"{OTA_MESSAGE_BATTERY_POWERED} {OTA_MESSAGE_RELIABILITY}"
+ ""
+ )
+
+ return f"{header}\n\n{self.entity_data.entity.release_notes or ''}"
@property
def release_url(self) -> str | None:
diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py
index cd48ae62ff3..c8cbc407106 100644
--- a/tests/components/zha/test_update.py
+++ b/tests/components/zha/test_update.py
@@ -1,6 +1,6 @@
"""Test ZHA firmware updates."""
-from unittest.mock import AsyncMock, call, patch
+from unittest.mock import AsyncMock, PropertyMock, call, patch
import pytest
from zha.application.platforms.update import (
@@ -14,6 +14,7 @@ from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters import general
+import zigpy.zdo.types as zdo_t
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
@@ -33,6 +34,10 @@ from homeassistant.components.zha.helpers import (
get_zha_gateway,
get_zha_gateway_proxy,
)
+from homeassistant.components.zha.update import (
+ OTA_MESSAGE_BATTERY_POWERED,
+ OTA_MESSAGE_RELIABILITY,
+)
from homeassistant.const import (
ATTR_ENTITY_ID,
STATE_OFF,
@@ -84,7 +89,26 @@ async def setup_test_data(
SIG_EP_PROFILE: zha.PROFILE_ID,
}
},
- node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00",
+ node_descriptor=zdo_t.NodeDescriptor(
+ logical_type=zdo_t.LogicalType.Router,
+ complex_descriptor_available=0,
+ user_descriptor_available=0,
+ reserved=0,
+ aps_flags=0,
+ frequency_band=zdo_t.NodeDescriptor.FrequencyBand.Freq2400MHz,
+ mac_capability_flags=(
+ zdo_t.NodeDescriptor.MACCapabilityFlags.FullFunctionDevice
+ | zdo_t.NodeDescriptor.MACCapabilityFlags.MainsPowered
+ | zdo_t.NodeDescriptor.MACCapabilityFlags.RxOnWhenIdle
+ | zdo_t.NodeDescriptor.MACCapabilityFlags.AllocateAddress
+ ),
+ manufacturer_code=4107,
+ maximum_buffer_size=82,
+ maximum_incoming_transfer_size=128,
+ server_mask=11264,
+ maximum_outgoing_transfer_size=128,
+ descriptor_capability_field=zdo_t.NodeDescriptor.DescriptorCapability.NONE,
+ ).serialize(),
)
gateway.get_or_create_device(zigpy_device)
@@ -568,27 +592,8 @@ async def test_update_release_notes(
) -> None:
"""Test ZHA update platform release notes."""
await setup_zha()
+ zha_device, _, _, _ = await setup_test_data(hass, zigpy_device_mock)
- gateway = get_zha_gateway(hass)
- gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass)
-
- zigpy_device = zigpy_device_mock(
- {
- 1: {
- SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id],
- SIG_EP_OUTPUT: [general.Ota.cluster_id],
- SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH,
- SIG_EP_PROFILE: zha.PROFILE_ID,
- }
- },
- node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00",
- )
-
- gateway.get_or_create_device(zigpy_device)
- await gateway.async_device_initialized(zigpy_device)
- await hass.async_block_till_done(wait_background_tasks=True)
-
- zha_device: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee)
zha_lib_entity = next(
e
for e in zha_device.device.platform_entities.values()
@@ -602,14 +607,39 @@ async def test_update_release_notes(
assert entity_id is not None
ws_client = await hass_ws_client(hass)
- await ws_client.send_json(
- {
- "id": 1,
- "type": "update/release_notes",
- "entity_id": entity_id,
- }
- )
- result = await ws_client.receive_json()
- assert result["success"] is True
- assert result["result"] == "Some lengthy release notes"
+ # Mains-powered devices
+ with patch(
+ "zha.zigbee.device.Device.is_mains_powered", PropertyMock(return_value=True)
+ ):
+ await ws_client.send_json(
+ {
+ "id": 1,
+ "type": "update/release_notes",
+ "entity_id": entity_id,
+ }
+ )
+
+ result = await ws_client.receive_json()
+ assert result["success"] is True
+ assert "Some lengthy release notes" in result["result"]
+ assert OTA_MESSAGE_RELIABILITY in result["result"]
+ assert OTA_MESSAGE_BATTERY_POWERED not in result["result"]
+
+ # Battery-powered devices
+ with patch(
+ "zha.zigbee.device.Device.is_mains_powered", PropertyMock(return_value=False)
+ ):
+ await ws_client.send_json(
+ {
+ "id": 2,
+ "type": "update/release_notes",
+ "entity_id": entity_id,
+ }
+ )
+
+ result = await ws_client.receive_json()
+ assert result["success"] is True
+ assert "Some lengthy release notes" in result["result"]
+ assert OTA_MESSAGE_RELIABILITY in result["result"]
+ assert OTA_MESSAGE_BATTERY_POWERED in result["result"]