Add consider home interval to ping (#104881)

* Add consider home interval to ping

* Run ruff after rebase

* Fix buggy consider home interval
This commit is contained in:
Jan-Philipp Benecke 2023-12-22 14:50:58 +01:00 committed by GitHub
parent a579a0c80a
commit 13504d5fd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 88 additions and 10 deletions

View File

@ -8,6 +8,10 @@ from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
@ -45,7 +49,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_HOST], title=user_input[CONF_HOST],
data={}, data={},
options={**user_input, CONF_PING_COUNT: DEFAULT_PING_COUNT}, options={
**user_input,
CONF_PING_COUNT: DEFAULT_PING_COUNT,
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.seconds,
},
) )
async def async_step_import(self, import_info: Mapping[str, Any]) -> FlowResult: async def async_step_import(self, import_info: Mapping[str, Any]) -> FlowResult:
@ -54,6 +62,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
to_import = { to_import = {
CONF_HOST: import_info[CONF_HOST], CONF_HOST: import_info[CONF_HOST],
CONF_PING_COUNT: import_info[CONF_PING_COUNT], CONF_PING_COUNT: import_info[CONF_PING_COUNT],
CONF_CONSIDER_HOME: import_info.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME
).seconds,
} }
title = import_info.get(CONF_NAME, import_info[CONF_HOST]) title = import_info.get(CONF_NAME, import_info[CONF_HOST])
@ -102,6 +113,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
min=1, max=100, mode=selector.NumberSelectorMode.BOX min=1, max=100, mode=selector.NumberSelectorMode.BOX
) )
), ),
vol.Optional(
CONF_CONSIDER_HOME,
default=self.config_entry.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.seconds
),
): int,
} }
), ),
) )

View File

@ -1,12 +1,15 @@
"""Tracks devices by sending a ICMP echo request (ping).""" """Tracks devices by sending a ICMP echo request (ping)."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
import voluptuous as vol import voluptuous as vol
from homeassistant.components.device_tracker import ( from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
AsyncSeeCallback, AsyncSeeCallback,
ScannerEntity, ScannerEntity,
@ -31,6 +34,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import PingDomainData from . import PingDomainData
from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DOMAIN from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DOMAIN
@ -91,6 +95,7 @@ async def async_setup_scanner(
CONF_NAME: dev_name, CONF_NAME: dev_name,
CONF_HOST: dev_host, CONF_HOST: dev_host,
CONF_PING_COUNT: config[CONF_PING_COUNT], CONF_PING_COUNT: config[CONF_PING_COUNT],
CONF_CONSIDER_HOME: config[CONF_CONSIDER_HOME],
}, },
) )
) )
@ -131,6 +136,8 @@ async def async_setup_entry(
class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity): class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity):
"""Representation of a Ping device tracker.""" """Representation of a Ping device tracker."""
_first_offline: datetime | None = None
def __init__( def __init__(
self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator
) -> None: ) -> None:
@ -139,6 +146,11 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
self._attr_name = config_entry.title self._attr_name = config_entry.title
self.config_entry = config_entry self.config_entry = config_entry
self._consider_home_interval = timedelta(
seconds=config_entry.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.seconds
)
)
@property @property
def ip_address(self) -> str: def ip_address(self) -> str:
@ -157,8 +169,16 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
@property @property
def is_connected(self) -> bool: def is_connected(self) -> bool:
"""Return true if ping returns is_alive.""" """Return true if ping returns is_alive or considered home."""
return self.coordinator.data.is_alive if self.coordinator.data.is_alive:
self._first_offline = None
return True
now = dt_util.utcnow()
if self._first_offline is None:
self._first_offline = now
return (self._first_offline + self._consider_home_interval) > now
@property @property
def entity_registry_enabled_default(self) -> bool: def entity_registry_enabled_default(self) -> bool:

View File

@ -5,8 +5,7 @@
"title": "Add Ping", "title": "Add Ping",
"description": "Ping allows you to check the availability of a host.", "description": "Ping allows you to check the availability of a host.",
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]"
"count": "Ping count"
}, },
"data_description": { "data_description": {
"host": "The hostname or IP address of the device you want to ping." "host": "The hostname or IP address of the device you want to ping."
@ -23,7 +22,11 @@
"init": { "init": {
"data": { "data": {
"host": "[%key:common::config_flow::data::host%]", "host": "[%key:common::config_flow::data::host%]",
"count": "[%key:component::ping::config::step::user::data::count%]" "count": "Ping count",
"consider_home": "Consider home interval"
},
"data_description": {
"consider_home": "Seconds to wait till marking a device tracker as not home after not being seen."
} }
} }
}, },

View File

@ -4,6 +4,7 @@ from unittest.mock import patch
from icmplib import Host from icmplib import Host
import pytest import pytest
from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME
from homeassistant.components.ping import DOMAIN from homeassistant.components.ping import DOMAIN
from homeassistant.components.ping.const import CONF_PING_COUNT from homeassistant.components.ping.const import CONF_PING_COUNT
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
@ -39,7 +40,11 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
title="10.10.10.10", title="10.10.10.10",
options={CONF_HOST: "10.10.10.10", CONF_PING_COUNT: 10.0}, options={
CONF_HOST: "10.10.10.10",
CONF_PING_COUNT: 10.0,
CONF_CONSIDER_HOME: 180,
},
) )

View File

@ -1,4 +1,6 @@
"""Constants for tests.""" """Constants for tests."""
from datetime import timedelta
from icmplib import Host from icmplib import Host
BINARY_SENSOR_IMPORT_DATA = { BINARY_SENSOR_IMPORT_DATA = {
@ -6,6 +8,7 @@ BINARY_SENSOR_IMPORT_DATA = {
"host": "127.0.0.1", "host": "127.0.0.1",
"count": 1, "count": 1,
"scan_interval": 50, "scan_interval": 50,
"consider_home": timedelta(seconds=240),
} }
NON_AVAILABLE_HOST_PING = Host("192.168.178.1", 10, []) NON_AVAILABLE_HOST_PING = Host("192.168.178.1", 10, [])

View File

@ -42,6 +42,7 @@ async def test_form(hass: HomeAssistant, host, expected_title) -> None:
assert result["options"] == { assert result["options"] == {
"count": 5, "count": 5,
"host": host, "host": host,
"consider_home": 180,
} }
@ -58,7 +59,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None
source=config_entries.SOURCE_USER, source=config_entries.SOURCE_USER,
data={}, data={},
domain=DOMAIN, domain=DOMAIN,
options={"count": count, "host": host}, options={"count": count, "host": host, "consider_home": 180},
title=expected_title, title=expected_title,
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -83,6 +84,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None
assert result["data"] == { assert result["data"] == {
"count": count, "count": count,
"host": "10.10.10.1", "host": "10.10.10.1",
"consider_home": 180,
} }
@ -103,6 +105,7 @@ async def test_step_import(hass: HomeAssistant) -> None:
assert result["options"] == { assert result["options"] == {
"host": "127.0.0.1", "host": "127.0.0.1",
"count": 1, "count": 1,
"consider_home": 240,
} }
# test import without name # test import without name
@ -119,4 +122,5 @@ async def test_step_import(hass: HomeAssistant) -> None:
assert result["options"] == { assert result["options"] == {
"host": "10.10.10.10", "host": "10.10.10.10",
"count": 5, "count": 5,
"consider_home": 180,
} }

View File

@ -1,6 +1,9 @@
"""Test the binary sensor platform of ping.""" """Test the binary sensor platform of ping."""
from datetime import timedelta
from unittest.mock import patch from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from icmplib import Host
import pytest import pytest
from homeassistant.components.device_tracker import legacy from homeassistant.components.device_tracker import legacy
@ -11,7 +14,7 @@ from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.yaml import dump from homeassistant.util.yaml import dump
from tests.common import MockConfigEntry, patch_yaml_files from tests.common import MockConfigEntry, async_fire_time_changed, patch_yaml_files
@pytest.mark.usefixtures("setup_integration") @pytest.mark.usefixtures("setup_integration")
@ -19,6 +22,7 @@ async def test_setup_and_update(
hass: HomeAssistant, hass: HomeAssistant,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None: ) -> None:
"""Test sensor setup and update.""" """Test sensor setup and update."""
@ -42,10 +46,32 @@ async def test_setup_and_update(
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# check device tracker is now "home"
state = hass.states.get("device_tracker.10_10_10_10") state = hass.states.get("device_tracker.10_10_10_10")
assert state.state == "home" assert state.state == "home"
with patch(
"homeassistant.components.ping.helpers.async_ping",
return_value=Host(address="10.10.10.10", packets_sent=10, rtts=[]),
):
# we need to travel two times into the future to run the update twice
freezer.tick(timedelta(minutes=1, seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
freezer.tick(timedelta(minutes=4, seconds=10))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get("device_tracker.10_10_10_10"))
assert state.state == "not_home"
freezer.tick(timedelta(minutes=1, seconds=1))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get("device_tracker.10_10_10_10"))
assert state.state == "home"
async def test_import_issue_creation( async def test_import_issue_creation(
hass: HomeAssistant, hass: HomeAssistant,