Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
skipperro committed Sep 9, 2023
0 parents commit b3245b5
Show file tree
Hide file tree
Showing 17 changed files with 388 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

.idea/

custom_components/ipify/
22 changes: 22 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@

The MIT License (MIT)

Copyright (c) 2023 Skipperro

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Ipify.org Public IP Check Integration

[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)
[![GitHub release](https://img.shields.io/github/release/skipperro/ipify-homeassistant.svg)](https://github.com/skipperro/ipify-homeassistant/releases/)
![](https://img.shields.io/badge/dynamic/json?color=41BDF5&logo=home-assistant&label=integration%20usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.ipify.total)


This integration allows you to check the public IP of your Home Assistant instance.

This is useful if you want to check if your Home Assistant instance is accessible from the outside
and can be used for automations like updating your DNS records after a new IP is assigned.

![ipify entity](images/publicipv4.png)

## How it works

Every 5 minutes the public IP is checked and if it changed, the integration will trigger a value change.

This value can be used in automation scripts or displayed on dashboards.

## Installation

1. Install this integration with HACS (adding repository required), or copy the contents of this
repository into the `custom_components/ipify` directory.
2. Restart Home Assistant.
3. Start the configuration flow:
- [![Start Config Flow](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start?domain=ipify)
- Or: Go to `Configuration` -> `Integrations` and click the `+ Add Integration`. Select `Ipify` from the list.
- If the integration is not found try to refresh the HA page without using cache (Ctrl+F5).
4. Select which IP protocol versions (IPv4 and/or IPv6) you want to check.

![ipify config](images/ipconfig.png)

## ToDo

- Add extra parameter to allow custom interval for checking IP.
- Promote this integration to `HACS Default` and/or `HA Core`.

## Credits

- Randall Degges (https://github.com/rdegges): Ipify-api code hosted on ipify.org that allows this integration to work.
- Skipperro: Creating the integration for Home Assistant.
58 changes: 58 additions & 0 deletions custom_components/pajgps/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""IP Custom Component."""
import asyncio
import logging

from homeassistant import config_entries, core

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up platform from a ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
hass_data = dict(entry.data)
# Registers update listener to update config entry when options are updated.
unsub_options_update_listener = entry.add_update_listener(options_update_listener)
# Store a reference to the unsubscribe function to cleanup if an entry is unloaded.
hass_data["unsub_options_update_listener"] = unsub_options_update_listener
hass.data[DOMAIN][entry.entry_id] = hass_data

# Forward the setup to the sensor platform.
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True


async def options_update_listener(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)


async def async_unload_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[hass.config_entries.async_forward_entry_unload(entry, "sensor")]
)
)
# Remove options_update_listener.
hass.data[DOMAIN][entry.entry_id]["unsub_options_update_listener"]()

# Remove config entry from domain.
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok

async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
hass.data.setdefault(DOMAIN, {})
return True
86 changes: 86 additions & 0 deletions custom_components/pajgps/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Config flow for PAJ GPS Tracker integration."""
from __future__ import annotations
import logging
from typing import Any, Dict, Optional
import homeassistant.helpers.config_validation as cv
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.core import callback

from .const import DOMAIN

big_int = vol.All(vol.Coerce(int), vol.Range(min=300))

_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema(
{
vol.Required('email', default=''): cv.string,
vol.Required('password', default=''): cv.string,
}
)

class CustomFlow(config_entries.ConfigFlow, domain=DOMAIN):
data: Optional[Dict[str, Any]]

async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
errors: Dict[str, str] = {}
if user_input is not None:
self.data = user_input
# If email is null or empty string, add error
if not self.data['email'] or self.data['email'] == '':
errors['base'] = 'email_required'
# If password is null or empty string, add error
if not self.data['password'] or self.data['password'] == '':
errors['base'] = 'password_required'
if not errors:
return self.async_create_entry(title="PAJ GPS Tracker", data=self.data)

return self.async_show_form(step_id="user", data_schema=CONFIG_SCHEMA, errors=errors)

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)

class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handles options flow for the component."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self.config_entry = config_entry

async def async_step_init(
self, user_input: Dict[str, Any] = None
) -> Dict[str, Any]:
errors: Dict[str, str] = {}

if user_input is not None:
self.data = user_input
# If email is null or empty string, add error
if not self.data['email'] or self.data['email'] == '':
errors['base'] = 'email_required'
# If password is null or empty string, add error
if not self.data['password'] or self.data['password'] == '':
errors['base'] = 'password_required'
if not errors:
return self.async_create_entry(title="PAJ GPS Tracker", data={'email': user_input['email'], 'password': user_input['password']})

default_email = ''
if 'email' in self.config_entry.data:
default_email = self.config_entry.data['email']
if 'email' in self.config_entry.options:
default_email = self.config_entry.options['email']
default_password = False
if 'password' in self.config_entry.data:
default_password = self.config_entry.data['password']
if 'password' in self.config_entry.options:
default_password = self.config_entry.options['password']

OPTIONS_SCHEMA = vol.Schema(
{
vol.Required('email', default=default_email): cv.string,
vol.Required('password', default=default_password): cv.string,
}
)
return self.async_show_form(step_id="init", data_schema=OPTIONS_SCHEMA, errors=errors)
1 change: 1 addition & 0 deletions custom_components/pajgps/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DOMAIN = "pajgps"
11 changes: 11 additions & 0 deletions custom_components/pajgps/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "pajgps",
"name": "PAJ GPS Tracker",
"documentation": "https://github.com/Skipperro/pajgps-homeassistant",
"dependencies": [],
"codeowners": ["Skipperro"],
"requirements": [],
"iot_class": "cloud_polling",
"config_flow": true,
"version": "0.1.0"
}
34 changes: 34 additions & 0 deletions custom_components/pajgps/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"config": {
"error": {
"email_required": "Please enter your e-mail address.",
"password_required": "Please enter your password."
},
"step": {
"user": {
"data": {
"email": "E-Mail",
"password": "Password"
},
"description": "Add credentials for PAJ GPS Tracker (finder-portal.com).",
"title": "Authorization"
}
}
},
"options": {
"error": {
"email_required": "Please enter your e-mail address.",
"password_required": "Please enter your password."
},
"step": {
"user": {
"data": {
"email": "E-Mail",
"password": "Password"
},
"description": "Add credentials for PAJ GPS Tracker (finder-portal.com).",
"title": "Authorization"
}
}
}
}
Empty file.
46 changes: 46 additions & 0 deletions custom_components/pajgps/tests/test_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest
import custom_components.ipify.sensor as sensor

class IPSensorTest(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
pass

def test_validate_ipv4(self):
assert sensor.validate_ipv4('192.168.0.1')
assert sensor.validate_ipv4('123.123.123.123')
assert not sensor.validate_ipv4('300.0.0.1')
assert not sensor.validate_ipv4('300.0.0.-1')
assert not sensor.validate_ipv4('2001:0db8:85a3:0000:0000:8a2e:0370:7334')
assert not sensor.validate_ipv4('1.2.3')
assert not sensor.validate_ipv4('1.2.3.4.5')
assert not sensor.validate_ipv4('abc.def.ghi.jkl')
assert not sensor.validate_ipv4('192.168.abc.def')
assert not sensor.validate_ipv4('')

def test_validate_ipv6(self):
assert sensor.validate_ipv6('2001:db8::2:1')
assert not sensor.validate_ipv6('2001::2:1')
assert sensor.validate_ipv6('2001:0db8:85a3:0000:0000:8a2e:0370:7334')
assert not sensor.validate_ipv6('2001:0db8:85a3:00000:0000:8a2e:0370:7334')
assert not sensor.validate_ipv6('2001:0db8:85a3:0000:0000:8a2e:0370:ghij')
assert not sensor.validate_ipv6('300.0.0.1')
assert not sensor.validate_ipv6('300.0.0.-1')
assert not sensor.validate_ipv6('192.168.0.1')
assert not sensor.validate_ipv6('1.2.3')
assert not sensor.validate_ipv6('1.2.3.4.5')
assert not sensor.validate_ipv6('')

async def test_update_ipv4(self):
await self.ipv4.async_update()
if self.ipv4.native_value == None:
return # skip tests if no ipv4 address is found (dev is offline?)
assert sensor.validate_ipv4(self.ipv4.native_value)

async def test_update_ipv6(self):
await self.ipv6.async_update()
if self.ipv6.native_value == None:
return # skip tests if no ipv6 address is found (dev is offline or uses only ipv4?)
assert len(self.ipv6.native_value) > 5
isv4 = sensor.validate_ipv4(self.ipv6.native_value)
if not isv4:
assert sensor.validate_ipv6(self.ipv6.native_value)
34 changes: 34 additions & 0 deletions custom_components/pajgps/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"config": {
"error": {
"email_required": "Please enter your e-mail address.",
"password_required": "Please enter your password."
},
"step": {
"user": {
"data": {
"email": "E-Mail",
"password": "Password"
},
"description": "Add credentials for PAJ GPS Tracker (finder-portal.com).",
"title": "Authorization"
}
}
},
"options": {
"error": {
"email_required": "Please enter your e-mail address.",
"password_required": "Please enter your password."
},
"step": {
"user": {
"data": {
"email": "E-Mail",
"password": "Password"
},
"description": "Add credentials for PAJ GPS Tracker (finder-portal.com).",
"title": "Authorization"
}
}
}
}
6 changes: 6 additions & 0 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "PAJ GPS Tracker",
"iot_class": "Cloud Polling",
"render_readme": true,
"zip_release": false
}
Binary file added images/dark_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/ipconfig.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/publicipv4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b3245b5

Please sign in to comment.