Skip to content

Commit 89ca8b3

Browse files
authored
Merge pull request #131 from mudape/vidare
Tuning
2 parents 65337b7 + 0ff873e commit 89ca8b3

File tree

4 files changed

+184
-147
lines changed

4 files changed

+184
-147
lines changed

README.md

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,94 @@
1-
[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs)
2-
![GitHub release](https://img.shields.io/github/release/mudape/iphonedetect.svg)
3-
# iPhone Detect
4-
This integration sends a message to the defined hosts on UDP port 5353.
5-
The iPhone responds, _even when in deep sleep_, and an entry in the ARP cache is made .
1+
# iPhone Detect for Home-Assistant
62

7-
Uses Home Assistant's [device_tracker](https://www.home-assistant.io/components/device_tracker/) and idea/script from [return01](https://community.home-assistant.io/u/return01)
3+
[![Validate with hassfest](https://github.com/mudape/iphonedetect/actions/workflows/hassfest.yaml/badge.svg)](https://github.com/mudape/iphonedetect/actions/workflows/hassfest.yaml)
4+
[![HACS Validate](https://github.com/mudape/iphonedetect/actions/workflows/hacs_action.yml/badge.svg)](https://github.com/mudape/iphonedetect/actions/workflows/hacs_action.yml)
5+
[![hacs_badge](https://img.shields.io/badge/HACS-Default-blue.svg)](https://github.com/custom-components/hacs)
86

9-
Only **IP addresses** will work, _no hostnames_!
10-
You have to assign a **static** IP address(es) to your iPhones, probably in your router.
7+
![downloads](https://img.shields.io/github/downloads/mudape/iphonedetect/total.svg)
8+
![downloads](https://img.shields.io/github/downloads/mudape/iphonedetect/1.4.2/total.svg)
9+
![downloads](https://img.shields.io/github/downloads/mudape/iphonedetect/latest/total.svg)
1110

12-
_The interval_seconds time must be shorter than the timeout in which the ARP cache is cleared (usally 15-45sec), or the phone will be marked not_home._
13-
_So, leave it at the default value (12sec) or make it shorter._
11+
This integration sends a mDNS message to defined hosts.
12+
The device responds if it's connected to the network, even when in deep sleep, and an entry in the ARP cache is made and read.
13+
Usefull as a [device_tracker](https://www.home-assistant.io/integrations/device_tracker/) for your [person](https://www.home-assistant.io/integrations/person/) integration to see if users are at home.
1414

15-
<a href="https://www.buymeacoffee.com/MudApe" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
15+
## Installation
16+
17+
After installation you need to **restart** Home-Assistant before using the integration.
18+
19+
### Using HACS
20+
21+
If you dont' have [HACS](https://hacs.xyz) installed yet, I highly recommend it.
22+
[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=mudape&repository=iphonedetect&category=integration) or search for `iPhone Device Tracker`.
23+
24+
### Manual
25+
26+
Download the `iphonedetect` directory and place in your `<config>/custom_component`
27+
28+
## Configuration
29+
30+
### Network
31+
32+
Assign a **static** IP address to the device you want to track, probably in your router.
33+
Alternative, set a manual IP in your device WiFi configuration.
34+
Avoid automatically connect to differtent SSID's (2.4 and 5 ghz bands)
35+
36+
### Setup
37+
38+
[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=iphonedetect)
39+
40+
If you don't have [My Home Assistant](https://my.home-assistant.io/) redirects set up, go to Settings -> Devices & Services
41+
Click "Add integration" and search for `iPhone Device Tracker`
42+
43+
Give the entity a unique `name` and enter its IP address.
44+
45+
![image](https://github.com/user-attachments/assets/c4583955-72d8-4c3a-81b4-062a134af11e)
1646

17-
## Example configuration.yaml
18-
19-
```yaml
20-
device_tracker:
21-
- platform: iphonedetect
22-
consider_home: 60
23-
scan_interval: 12
24-
new_device_defaults:
25-
track_new_devices: true
26-
hosts:
27-
hostname1: 192.168.0.17
28-
hostname2: 192.168.0.24
29-
```
30-
This will create `device_tracker.hostname1` and `device_tracker.hostname2` once the devices have been detected on your network.
31-
(Re)start the Wi-Fi on your device/phone to trigger their creation on first run.
32-
33-
__Note__
34-
If you have `track_new_devices: false` (in this or any integrations specified before this) for the device_tracker component you need to manually change `track:` to true for each device in `known_devices.yaml`
35-
(see component settings for [device_tracker](https://www.home-assistant.io/components/device_tracker/#configuring-a-device_tracker-platform))
36-
```yaml
37-
hostname1:
38-
icon:
39-
mac:
40-
name: hostname1
41-
picture:
42-
track: true
43-
```
47+
This will create an entity `device_tracker.<name>`
4448

49+
Repeat the steps for additional entities.
50+
51+
#### Options
52+
53+
You can change the consider home timeout per tracked device in the UI.
54+
Default is 24 seconds.
55+
![image](https://github.com/user-attachments/assets/caf31775-333d-448c-b8f2-660534d856d7)
56+
57+
#### Reconfigure
58+
59+
You can change the IP address of the tracked device in the UI.
60+
![image](https://github.com/user-attachments/assets/4fc98224-eee6-4451-aaf1-b16c858014d2)
61+
62+
## Troubleshooting | FAQ
63+
64+
<details>
65+
<summary>Private Wi-Fi address</summary>
66+
The device private MAC address will remain the same for each network, as long as the network isn't recreated or rotating (iOS 18+) MAC is used.
67+
68+
In other words, if a tracked device no longer can be found the user have propably recreated the connection to your Wi-Fi forcing a new random MAC address to be used.
69+
<br />
70+
Note that if your WiFi has different SSID for the 2.4 and 5 ghz bands, and the phone has private network enabled, two different MAC will be presented to the DHCP.
71+
Most likely that will require the integration to track two IP's per user/Person.
72+
<br />
73+
So, for best result only have/connect to one SSID, turn off private network and assign static IP.
74+
</details>
75+
<details>
76+
<summary>False or flaky not_home status</summary>
77+
Especially when the network has multiple Access Points, tracked devices sometimes are marked as not_home when actually connected to the network.
78+
Increasing the consider home timeout might help this.
79+
Most likely it's a network issue though, try placing your AP's at different locations.
80+
81+
Devices might auto-update during night, they of course then will be not_home for awhile.
82+
</details>
83+
84+
## Attribution
85+
86+
Original idea from [return01](https://community.home-assistant.io/u/return01)
87+
88+
## Disclaimer
89+
90+
Author is in no way affiliated with Apple Inc.
91+
Author does not guarantee functionality of this integration and is not responsible for any damage.
92+
All product names, trademarks and registered trademarks in this repository, are property of their respective owners.
93+
94+
<a href="https://www.buymeacoffee.com/MudApe" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>

custom_components/iphonedetect/config_flow.py

Lines changed: 73 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414
ConfigEntry,
1515
ConfigFlow,
1616
FlowResult,
17-
OptionsFlow,
1817
)
1918
from homeassistant.const import (
2019
CONF_IP_ADDRESS,
2120
CONF_NAME,
2221
)
2322
from homeassistant.core import HomeAssistant, callback
23+
from homeassistant.helpers.schema_config_entry_flow import (
24+
SchemaFlowFormStep,
25+
SchemaOptionsFlowHandler,
26+
)
2427
from homeassistant.util import slugify
2528

2629
from .const import (
@@ -30,28 +33,31 @@
3033
MIN_CONSIDER_HOME,
3134
)
3235

33-
_LOGGER = logging.getLogger(__name__)
34-
3536

36-
async def validate_ip_address(subnets: list[IPv4Network], devices: list, ip: str) -> dict:
37-
"""Try to validate user input"""
38-
errors = {}
37+
_LOGGER = logging.getLogger(__name__)
3938

40-
if ip in devices:
41-
errors["base"] = "ip_already_configured"
4239

43-
if not errors:
44-
try:
45-
ip_address = IPv4Address(ip)
46-
except AddressValueError:
47-
errors["base"] = "ip_invalid"
48-
return errors
40+
OPTIONS_SCHEMA = vol.Schema(
41+
{
42+
vol.Required(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All(
43+
vol.Coerce(int),
44+
vol.Range(min=MIN_CONSIDER_HOME, max=MAX_CONSIDER_HOME),
45+
)
46+
}
47+
)
4948

50-
if not errors:
51-
if not any(ip_address in subnet for subnet in subnets):
52-
errors["base"] = "ip_range"
49+
DATA_SCHEMA = vol.Schema(
50+
{
51+
vol.Required(CONF_NAME, description={"suggested_value": "My iPhone"}): str,
52+
vol.Required(CONF_IP_ADDRESS, description={"suggested_value": "192.168.1.xx"}): str,
53+
**OPTIONS_SCHEMA.schema,
54+
vol.Optional("subnet_check", default=True): bool,
55+
}
56+
)
5357

54-
return errors
58+
OPTIONS_FLOW = {
59+
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
60+
}
5561

5662

5763
async def async_get_networks(hass: HomeAssistant) -> list[IPv4Network]:
@@ -68,81 +74,72 @@ async def async_get_networks(hass: HomeAssistant) -> list[IPv4Network]:
6874
return networks
6975

7076

71-
class IphoneDetectOptionsFlowHandler(OptionsFlow):
72-
"""iPhone Detect config flow options handler."""
73-
74-
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult:
75-
"""Manage the options."""
76-
return await self.async_step_user(user_input)
77-
78-
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult:
79-
"""Handle options flow."""
80-
errors = {}
81-
82-
if user_input is not None and user_input[CONF_CONSIDER_HOME] != self.config_entry.options[CONF_CONSIDER_HOME]:
83-
new_options = self.config_entry.options | {CONF_CONSIDER_HOME: user_input[CONF_CONSIDER_HOME]}
84-
return self.async_create_entry(title="", data=new_options)
85-
86-
return self.async_show_form(
87-
step_id="user",
88-
data_schema=vol.Schema(
89-
{
90-
vol.Required(
91-
CONF_CONSIDER_HOME,
92-
default=self.config_entry.options[CONF_CONSIDER_HOME],
93-
): vol.All(
94-
vol.Coerce(int),
95-
vol.Range(min=MIN_CONSIDER_HOME, max=MAX_CONSIDER_HOME),
96-
),
97-
}
98-
),
99-
errors=errors,
100-
)
77+
async def _validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, str] | None:
78+
"""Try to validate user input"""
79+
entries = [entry for entry in hass.config_entries.async_entries(DOMAIN)]
80+
ip = user_input[CONF_IP_ADDRESS]
81+
82+
# Check if name already used for a clearer error
83+
if user_input.get(CONF_NAME, False):
84+
entries_id = [entry.unique_id for entry in entries]
85+
entry_id = f"{DOMAIN}_{slugify(user_input[CONF_NAME]).lower()}"
86+
if entry_id in entries_id:
87+
return {"base": "name_not_unique"}
88+
89+
# Check if valid IP address
90+
try:
91+
IPv4Address(ip)
92+
except AddressValueError:
93+
return {"base": "ip_invalid"}
94+
95+
# Check if IP address already used for a clearer error
96+
entries_ip = [entry.options[CONF_IP_ADDRESS] for entry in entries]
97+
if ip in entries_ip:
98+
return {"base": "ip_already_configured"}
99+
100+
# Check if device IP will be seen by ARP
101+
if user_input["subnet_check"]:
102+
subnets = await async_get_networks(hass)
103+
if not any(IPv4Address(ip) in subnet for subnet in subnets):
104+
return {"base": "ip_range"}
101105

102106

103107
class IphoneDetectFlowHandler(ConfigFlow, domain=DOMAIN): # type: ignore
104108
"""Handle a config flow."""
105109

106110
VERSION = 2
107111

112+
@staticmethod
113+
@callback
114+
def async_get_options_flow(config_entry: ConfigEntry) -> SchemaOptionsFlowHandler:
115+
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
116+
108117
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult:
109118
"""Handle a flow initialized by the user."""
110119
errors = {}
111120

112121
if user_input is not None:
113-
ip = user_input[CONF_IP_ADDRESS]
114-
devices = [dev.options[CONF_IP_ADDRESS] for dev in self.hass.config_entries.async_entries(DOMAIN)]
115-
subnet = await async_get_networks(self.hass)
116-
117-
errors = await validate_ip_address(subnet, devices, ip)
122+
errors = await _validate_input(self.hass, user_input)
118123

119124
if not errors:
120125
unique_id = slugify(user_input[CONF_NAME]).lower()
121126
await self.async_set_unique_id(f"{DOMAIN}_{unique_id}")
122127
self._abort_if_unique_id_configured()
123128

124129
self._async_abort_entries_match({CONF_NAME: user_input[CONF_NAME]})
130+
125131
return self.async_create_entry(
126132
title=user_input[CONF_NAME],
127133
data={},
128134
options={
129-
CONF_IP_ADDRESS: ip,
135+
CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS],
130136
CONF_CONSIDER_HOME: user_input[CONF_CONSIDER_HOME],
131137
},
132138
)
133139

134140
return self.async_show_form(
135141
step_id="user",
136-
data_schema=vol.Schema(
137-
{
138-
vol.Required(CONF_NAME, description={"suggested_value": "My iPhone"}): str,
139-
vol.Required(CONF_IP_ADDRESS, description={"suggested_value": "192.168.1.xx"}): str,
140-
vol.Required(CONF_CONSIDER_HOME, default=DEFAULT_CONSIDER_HOME): vol.All(
141-
vol.Coerce(int),
142-
vol.Range(min=MIN_CONSIDER_HOME, max=MAX_CONSIDER_HOME),
143-
),
144-
}
145-
),
142+
data_schema=self.add_suggested_values_to_schema(DATA_SCHEMA, user_input),
146143
errors=errors,
147144
)
148145

@@ -167,38 +164,24 @@ async def async_step_import(self, import_config) -> FlowResult:
167164

168165
async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> FlowResult:
169166
"""Handle options flow."""
170-
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
171-
assert entry
172-
173167
errors = {}
174-
if user_input is not None:
175-
new_ip = user_input.get(CONF_IP_ADDRESS)
176-
current_ip = entry.options[CONF_IP_ADDRESS]
177-
178-
if new_ip and new_ip != current_ip:
179-
devices = [other_entry.options[CONF_IP_ADDRESS] for other_entry in self.hass.config_entries.async_entries(DOMAIN) if other_entry.entry_id != entry.entry_id]
180-
subnets = await async_get_networks(self.hass)
168+
entry = self._get_reconfigure_entry()
181169

182-
errors = await validate_ip_address(subnets, devices, new_ip)
170+
if user_input is not None:
171+
errors = await _validate_input(self.hass, user_input)
172+
if not errors:
173+
await self.async_set_unique_id(entry.unique_id)
174+
self._abort_if_unique_id_mismatch()
175+
new_options = entry.options | {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]}
183176

184-
if not errors:
185-
new_options = entry.options | {
186-
CONF_IP_ADDRESS: new_ip,
187-
}
188-
return self.async_update_reload_and_abort(entry, options=new_options, reason="reconfigure_successful")
177+
return self.async_update_reload_and_abort(
178+
entry,
179+
options=new_options,
180+
)
189181

190182
return self.async_show_form(
191183
step_id="reconfigure",
192-
data_schema=vol.Schema(
193-
{
194-
vol.Required(CONF_IP_ADDRESS, default=entry.options[CONF_IP_ADDRESS]): str,
195-
}
196-
),
184+
data_schema=vol.Schema({vol.Required(CONF_IP_ADDRESS, default=entry.options[CONF_IP_ADDRESS]): str, vol.Optional("subnet_check", default=True): bool}),
197185
description_placeholders={"device_name": entry.title},
198186
errors=errors,
199187
)
200-
201-
@staticmethod
202-
@callback
203-
def async_get_options_flow(config_entry: ConfigEntry) -> IphoneDetectOptionsFlowHandler:
204-
return IphoneDetectOptionsFlowHandler()

0 commit comments

Comments
 (0)