diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index dcc2a080a76..ecb65981637 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -137,10 +137,23 @@ CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" CONF_ZIGPY = "zigpy_config" +CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" +CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours +CONF_CONSIDER_UNAVAILABLE_BATTERY = "consider_unavailable_battery" +CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours + CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION): cv.positive_int, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_MAINS, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + ): cv.positive_int, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_BATTERY, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + ): cv.positive_int, } ) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 37608287609..0c572bfba8a 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -55,6 +55,10 @@ from .const import ( CLUSTER_COMMANDS_SERVER, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + CONF_CONSIDER_UNAVAILABLE_BATTERY, + CONF_CONSIDER_UNAVAILABLE_MAINS, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, CONF_ENABLE_IDENTIFY_ON_JOIN, EFFECT_DEFAULT_VARIANT, EFFECT_OKAY, @@ -70,8 +74,6 @@ from .const import ( from .helpers import LogMixin, async_get_zha_config_value _LOGGER = logging.getLogger(__name__) -CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours -CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours _UPDATE_ALIVE_INTERVAL = (60, 90) _CHECKIN_GRACE_PERIODS = 2 @@ -107,9 +109,20 @@ class ZHADevice(LogMixin): ) if self.is_mains_powered: - self._consider_unavailable_time = CONSIDER_UNAVAILABLE_MAINS + self.consider_unavailable_time = async_get_zha_config_value( + self._zha_gateway.config_entry, + ZHA_OPTIONS, + CONF_CONSIDER_UNAVAILABLE_MAINS, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + ) else: - self._consider_unavailable_time = CONSIDER_UNAVAILABLE_BATTERY + self.consider_unavailable_time = async_get_zha_config_value( + self._zha_gateway.config_entry, + ZHA_OPTIONS, + CONF_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + ) + keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) self.unsubs.append( async_track_time_interval( @@ -320,7 +333,7 @@ class ZHADevice(LogMixin): return difference = time.time() - self.last_seen - if difference < self._consider_unavailable_time: + if difference < self.consider_unavailable_time: self.update_available(True) self._checkins_missed_count = 0 return diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4a9e6c28203..3ba5627cde8 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -77,12 +77,7 @@ from .const import ( ZHA_GW_MSG_RAW_INIT, RadioType, ) -from .device import ( - CONSIDER_UNAVAILABLE_BATTERY, - CONSIDER_UNAVAILABLE_MAINS, - DeviceStatus, - ZHADevice, -) +from .device import DeviceStatus, ZHADevice from .group import GroupMember, ZHAGroup from .registries import GROUP_ENTITY_DOMAINS from .store import async_get_registry @@ -185,17 +180,15 @@ class ZHAGateway: delta_msg = "not known" if zha_dev_entry and zha_dev_entry.last_seen is not None: delta = round(time.time() - zha_dev_entry.last_seen) - if zha_device.is_mains_powered: - zha_device.available = delta < CONSIDER_UNAVAILABLE_MAINS - else: - zha_device.available = delta < CONSIDER_UNAVAILABLE_BATTERY + zha_device.available = delta < zha_device.consider_unavailable_time delta_msg = f"{str(timedelta(seconds=delta))} ago" _LOGGER.debug( - "[%s](%s) restored as '%s', last seen: %s", + "[%s](%s) restored as '%s', last seen: %s, consider_unavailable_time: %s seconds", zha_device.nwk, zha_device.name, "available" if zha_device.available else "unavailable", delta_msg, + zha_device.consider_unavailable_time, ) # update the last seen time for devices every 10 minutes to avoid thrashing # writes and shutdown issues where storage isn't updated diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 9ca6a9821b3..9abff4e83e2 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -33,7 +33,9 @@ "zha_options": { "title": "Global Options", "enable_identify_on_join": "Enable identify effect when devices join the network", - "default_light_transition": "Default light transition time (seconds)" + "default_light_transition": "Default light transition time (seconds)", + "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", + "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)" }, "zha_alarm_options": { "title": "Alarm Control Panel Options", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 83dc56c75fc..cceb6cfdde6 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -43,6 +43,8 @@ "zha_options": { "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", + "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", + "consider_unavailable_battery": "Consider battery powered devices unavailable after (seconds)", "title": "Global Options" } }, diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index d3ab3c3ada2..d5ed0152b8b 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -8,7 +8,10 @@ import pytest import zigpy.profiles.zha import zigpy.zcl.clusters.general as general -import homeassistant.components.zha.core.device as zha_core_device +from homeassistant.components.zha.core.const import ( + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, +) from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE import homeassistant.helpers.device_registry as dr import homeassistant.util.dt as dt_util @@ -117,13 +120,13 @@ async def test_check_available_success( basic_ch.read_attributes.reset_mock() device_with_basic_channel.last_seen = None assert zha_device.available is True - _send_time_changed(hass, zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2) + _send_time_changed(hass, zha_device.consider_unavailable_time + 2) await hass.async_block_till_done() assert zha_device.available is False assert basic_ch.read_attributes.await_count == 0 device_with_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) _seens = [time.time(), device_with_basic_channel.last_seen] @@ -172,7 +175,7 @@ async def test_check_available_unsuccessful( assert basic_ch.read_attributes.await_count == 0 device_with_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) # unsuccessfuly ping zigpy device, but zha_device is still available @@ -213,7 +216,7 @@ async def test_check_available_no_basic_channel( assert zha_device.available is True device_without_basic_channel.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2 + time.time() - zha_device.consider_unavailable_time - 2 ) assert "does not have a mandatory basic cluster" not in caplog.text @@ -246,38 +249,38 @@ async def test_ota_sw_version(hass, ota_zha_device): ("zigpy_device", 0, True), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, True, ), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, True, ), ( "zigpy_device", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, False, ), ("zigpy_device_mains", 0, True), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS - 2, True, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_MAINS + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, False, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, False, ), ( "zigpy_device_mains", - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 2, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, False, ), ), diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index b2f964e2695..841d6b43400 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -7,7 +7,6 @@ import zigpy.profiles.zha import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation -import homeassistant.components.zha.core.device as zha_core_device from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -252,9 +251,7 @@ async def test_device_offline_fires( await hass.async_block_till_done() assert zha_device.available is True - zigpy_device.last_seen = ( - time.time() - zha_core_device.CONSIDER_UNAVAILABLE_BATTERY - 2 - ) + zigpy_device.last_seen = time.time() - zha_device.consider_unavailable_time - 2 # there are 3 checkins to perform before marking the device unavailable future = dt_util.utcnow() + timedelta(seconds=90) @@ -266,7 +263,7 @@ async def test_device_offline_fires( await hass.async_block_till_done() future = dt_util.utcnow() + timedelta( - seconds=zha_core_device.CONSIDER_UNAVAILABLE_BATTERY + 100 + seconds=zha_device.consider_unavailable_time + 100 ) async_fire_time_changed(hass, future) await hass.async_block_till_done()