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
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.core import callback
from homeassistant.data_entry_flow import FlowResult
@ -45,7 +49,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_create_entry(
title=user_input[CONF_HOST],
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:
@ -54,6 +62,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
to_import = {
CONF_HOST: import_info[CONF_HOST],
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])
@ -102,6 +113,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
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)."""
from __future__ import annotations
from datetime import datetime, timedelta
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME,
PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA,
AsyncSeeCallback,
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.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import PingDomainData
from .const import CONF_IMPORTED_BY, CONF_PING_COUNT, DOMAIN
@ -91,6 +95,7 @@ async def async_setup_scanner(
CONF_NAME: dev_name,
CONF_HOST: dev_host,
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):
"""Representation of a Ping device tracker."""
_first_offline: datetime | None = None
def __init__(
self, config_entry: ConfigEntry, coordinator: PingUpdateCoordinator
) -> None:
@ -139,6 +146,11 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
self._attr_name = config_entry.title
self.config_entry = config_entry
self._consider_home_interval = timedelta(
seconds=config_entry.options.get(
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.seconds
)
)
@property
def ip_address(self) -> str:
@ -157,8 +169,16 @@ class PingDeviceTracker(CoordinatorEntity[PingUpdateCoordinator], ScannerEntity)
@property
def is_connected(self) -> bool:
"""Return true if ping returns is_alive."""
return self.coordinator.data.is_alive
"""Return true if ping returns is_alive or considered home."""
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
def entity_registry_enabled_default(self) -> bool:

View File

@ -5,8 +5,7 @@
"title": "Add Ping",
"description": "Ping allows you to check the availability of a host.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"count": "Ping count"
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the device you want to ping."
@ -23,7 +22,11 @@
"init": {
"data": {
"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
import pytest
from homeassistant.components.device_tracker.const import CONF_CONSIDER_HOME
from homeassistant.components.ping import DOMAIN
from homeassistant.components.ping.const import CONF_PING_COUNT
from homeassistant.const import CONF_HOST
@ -39,7 +40,11 @@ async def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
return MockConfigEntry(
domain=DOMAIN,
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."""
from datetime import timedelta
from icmplib import Host
BINARY_SENSOR_IMPORT_DATA = {
@ -6,6 +8,7 @@ BINARY_SENSOR_IMPORT_DATA = {
"host": "127.0.0.1",
"count": 1,
"scan_interval": 50,
"consider_home": timedelta(seconds=240),
}
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"] == {
"count": 5,
"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,
data={},
domain=DOMAIN,
options={"count": count, "host": host},
options={"count": count, "host": host, "consider_home": 180},
title=expected_title,
)
config_entry.add_to_hass(hass)
@ -83,6 +84,7 @@ async def test_options(hass: HomeAssistant, host, count, expected_title) -> None
assert result["data"] == {
"count": count,
"host": "10.10.10.1",
"consider_home": 180,
}
@ -103,6 +105,7 @@ async def test_step_import(hass: HomeAssistant) -> None:
assert result["options"] == {
"host": "127.0.0.1",
"count": 1,
"consider_home": 240,
}
# test import without name
@ -119,4 +122,5 @@ async def test_step_import(hass: HomeAssistant) -> None:
assert result["options"] == {
"host": "10.10.10.10",
"count": 5,
"consider_home": 180,
}

View File

@ -1,6 +1,9 @@
"""Test the binary sensor platform of ping."""
from datetime import timedelta
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from icmplib import Host
import pytest
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.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")
@ -19,6 +22,7 @@ async def test_setup_and_update(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry: MockConfigEntry,
freezer: FrozenDateTimeFactory,
) -> None:
"""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.async_block_till_done()
# check device tracker is now "home"
state = hass.states.get("device_tracker.10_10_10_10")
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(
hass: HomeAssistant,