├── hacs.json ├── custom_components └── satellitetracker │ ├── manifest.json │ ├── const.py │ ├── translations │ └── sk.json │ ├── strings.json │ ├── device_tracker.py │ ├── binary_sensor.py │ ├── __init__.py │ ├── sensor.py │ └── config_flow.py ├── .gitignore ├── info.md ├── README.md └── LICENSE /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Satellite Tracker (N2YO)", 3 | "country": "CA", 4 | "domains": ["binary_sensor", "device_tracker", "sensor"], 5 | "homeassistant": "0.115.0", 6 | "iot_class": ["Cloud Polling"] 7 | } -------------------------------------------------------------------------------- /custom_components/satellitetracker/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "satellitetracker", 3 | "name": "Satellite Tracker (N2YO)", 4 | "config_flow": true, 5 | "documentation": "https://github.com/djtimca/hasatellitetracker", 6 | "issue_tracker": "https://github.com/djtimca/hasatellitetracker/issues", 7 | "requirements": [ 8 | "n2yoasync==0.0.6" 9 | ], 10 | "ssdp": [], 11 | "zeroconf": [], 12 | "homekit": {}, 13 | "dependencies": [], 14 | "codeowners": [ 15 | "@djtimca" 16 | ], 17 | "version": "0.0.13" 18 | } -------------------------------------------------------------------------------- /custom_components/satellitetracker/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Satellite Tracker integration.""" 2 | 3 | DOMAIN = "satellitetracker" 4 | COORDINATOR = "coordinator" 5 | SATELLITE_API = "n2yo_api" 6 | ATTR_IDENTIFIERS = "identifiers" 7 | ATTR_MANUFACTURER = "manufacturer" 8 | ATTR_MODEL = "model" 9 | CONF_PASS_TYPE = "pass_type" 10 | CONF_MIN_VISIBILITY = "min_visibility" 11 | CONF_MIN_ELEVATION = "min_elevation" 12 | CONF_SATELLITE = "satellite" 13 | CONF_MIN_ALERT = "min_alert" 14 | DEFAULT_MIN_ALERT = 45 15 | DEFAULT_POLLING_INTERVAL = 30 16 | DEFAULT_MIN_VISIBILITY = 300 17 | DEFAULT_MIN_ELEVATION = 0 18 | TRACKER_TYPE = { 19 | "0": "location", 20 | "1": "satellite", 21 | } 22 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/translations/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "N2YO Sledovač satelitov", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "Na používanie tejto integrácie musíte získať API kľúč z https://n2yo.com/api.", 7 | "data": { 8 | "api_key": "Zadajte svoj N2YO.com API kľúč." 9 | } 10 | }, 11 | "type": { 12 | "data": { 13 | "type": "Vyberte typ senzorov ktoré chcete pridať. Satelit bude sledovať jednotlivý satelit, Poloha bude sledovať všetky satelity vo vašej zadanej polohe." 14 | } 15 | }, 16 | "location": { 17 | "data": { 18 | "name": "Zadajte názov pre túto polohu.", 19 | "category": "Vyberte kategóriu satelitov ktoré chcete sledovať.", 20 | "latitude": "Zadajte zemepisnú šírku (predvolená je vaša poloha Home Assistant).", 21 | "longitude": "Zadajte zemepisnú dĺžku.", 22 | "elevation": "Zadajte nadmorskú výšku od hladiny mora." 23 | } 24 | }, 25 | "satellite": { 26 | "data": { 27 | "name": "Zadajte názov satelitu.", 28 | "sat_id": "Zadajte NORAD ID satelitu (predvolené je ISS).", 29 | "latitude": "Zadajte zemepisnú šírku pre viditeľnosť (predvolená je vaša poloha Home Assistant).", 30 | "longitude": "Zadajte zemepisnú dĺžku.", 31 | "elevation": "Zadajte nadmorskú výšku od hladiny mora." 32 | } 33 | } 34 | }, 35 | "error": { 36 | "cannot_connect": "Nemožno sa pripojiť k N2YO.com.", 37 | "invalid_auth": "Neplatný API kľúč.", 38 | "unknown": "Vyskytol sa neznámy problém." 39 | } 40 | }, 41 | "options": { 42 | "step": { 43 | "init": { 44 | "data": { 45 | "scan_interval": "Zadajte interval dotazovania v sekundách.", 46 | "radius": "Zadajte polomer pre vaše senzory polohy nad hlavou.", 47 | "min_visibility": "Zadajte minimálny čas viditeľnosti pre prechody satelitu v sekundách.", 48 | "min_alert": "Zadajte minimálne stupne pre upozornenia na prechod." 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "N2YO Satellite Tracker", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "You must get an API key from https://n2yo.com/api to use this integration.", 7 | "data": { 8 | "api_key": "Enter your N2YO.com API Key." 9 | } 10 | }, 11 | "type": { 12 | "data": { 13 | "type": "Select the type of sensors to add. Satellite will track an individual satellite, Location will track all satellites at your specified location." 14 | } 15 | }, 16 | "location": { 17 | "data": { 18 | "name": "Enter the name for this location.", 19 | "category": "Select the category of satellites to track.", 20 | "latitude": "Enter the latitude (default your Home Assistant location).", 21 | "longitude": "Enter the longitude.", 22 | "elevation": "Enter the elevation from sea level." 23 | } 24 | }, 25 | "satellite": { 26 | "data": { 27 | "name": "Enter the name of the satellite.", 28 | "sat_id": "Enter the satellite NORAD ID (default the ISS).", 29 | "latitude": "Enter the latitude for visibility (default your Home Assistant location).", 30 | "longitude": "Enter the longitude.", 31 | "elevation": "Enter the elevation from sea level." 32 | } 33 | } 34 | }, 35 | "error": { 36 | "cannot_connect": "Can't connect to N2YO.com.", 37 | "invalid_auth": "Invalid API Key.", 38 | "unknown": "An unknown issue occurred." 39 | } 40 | }, 41 | "options": { 42 | "step": { 43 | "init": { 44 | "data": { 45 | "scan_interval": "Enter your polling interval in seconds.", 46 | "radius": "Enter the radius for your overhead location sensors.", 47 | "min_visibility": "Enter the minimum visibility time for satellite passes in seconds.", 48 | "min_alert": "Enter the minimum degrees for pass alerts." 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/device_tracker.py: -------------------------------------------------------------------------------- 1 | """Support for tracking a Satellite as device tracker.""" 2 | 3 | from homeassistant.components.device_tracker.config_entry import TrackerEntity 4 | from homeassistant.components.device_tracker.const import DOMAIN as DEVICE_TRACKER 5 | from homeassistant.components.device_tracker.const import SourceType 6 | from homeassistant.const import ATTR_NAME 7 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 8 | 9 | from . import N2YOSatelliteCoordinator 10 | 11 | from .const import ( 12 | DOMAIN, 13 | ATTR_IDENTIFIERS, 14 | ATTR_MANUFACTURER, 15 | ATTR_MODEL, 16 | COORDINATOR, 17 | ) 18 | 19 | 20 | async def async_setup_entry(hass, entry, async_add_entities): 21 | """Set up the device tracker platforms.""" 22 | 23 | coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] 24 | entities = [] 25 | 26 | entities.append(SatelliteTracker(coordinator=coordinator)) 27 | 28 | async_add_entities(entities) 29 | 30 | 31 | class SatelliteTracker(CoordinatorEntity, TrackerEntity): 32 | """Defines a satellite tracker.""" 33 | @property 34 | def unique_id(self): 35 | """Return the unique_id of the sensor.""" 36 | return f"{self.coordinator._satellite}_location" 37 | 38 | @property 39 | def available(self) -> bool: 40 | """Return if entity is available.""" 41 | return self.coordinator.last_update_success 42 | 43 | @property 44 | def icon(self): 45 | """Return the icon for the sensor.""" 46 | return "mdi:satellite-variant" 47 | 48 | @property 49 | def name(self): 50 | """Return the name of the sensor.""" 51 | return self.coordinator._name 52 | 53 | @property 54 | def latitude(self): 55 | """Return the current latitude of the satellite.""" 56 | return self.coordinator.data["positions"]["positions"][0]["satlatitude"] 57 | 58 | @property 59 | def longitude(self): 60 | """Return the current longitude of the satellite.""" 61 | return self.coordinator.data["positions"]["positions"][0]["satlongitude"] 62 | 63 | @property 64 | def source_type(self): 65 | """Return the source type of the client.""" 66 | return SourceType.GPS 67 | 68 | @property 69 | def device_info(self): 70 | """Define the device as a device tracker system.""" 71 | 72 | device_name = self.coordinator._name 73 | device_model = "Satellite Sensor" 74 | 75 | return { 76 | ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator._satellite)}, 77 | ATTR_NAME: device_name, 78 | ATTR_MANUFACTURER: "N2YO.com", 79 | ATTR_MODEL: device_model, 80 | } 81 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Custom Component Tracking Satellites 2 | 3 | Using the N2YO API, this Home Assistant integration provides visible satellite passes (general) and to add specific satellites for monitoring. 4 | 5 | You will need to register for an API Key at . 6 | 7 | ## Configuration 8 | 9 | Go to Configuration -> Integrations and click the plus sign to add a Satellite Tracker integration. Search for Satellite Tracker (N2YO) and click add. 10 | 11 | You will first need to enter your API key. 12 | 13 | Second you will choose the type of integration you want to install: 14 | 15 | ### Location 16 | 17 | A location integration will add a single sensor which will show you the number of satellites which are currently overhead, and in the attributes will list the satellites and their current positions. 18 | 19 | If you select this option, on the next page you will enter: 20 | 21 | - The name of the location you are tracking (default: the name of your Home Assistant instance) 22 | - The category of satellites you want to track (default: all) 23 | - The latitude of the location you want to track above (default: your Home Assistant latitude) 24 | - The longitude of the location you want to track above (default: your Home Assistant longitude) 25 | - The elevation of the location you want to track above (default: your Home Assistant elevation) 26 | 27 | ### Satellite 28 | 29 | A satellite integration will add 5 sensors for the next visible passes and a device tracker sensor to allow you to plot the current location of a specific satellite on your map. 30 | 31 | If you select this option, on the next page you will enter: 32 | 33 | - The name of the satellite you want to track (default: International Space Station (ISS)) 34 | - The NORAD ID (from n2yo.com) you want to track (default: 25544 for the ISS) 35 | - The latitude for visible pass tracking (default: Your Home Assistant latitude) 36 | - The longitude for visible pass tracking (default: Your Home Assistant longitude) 37 | - The elevation for visible pass tracking (default: Your Home Assistant elevation) 38 | 39 | ## Options 40 | 41 | One you have set up an instance of the integration you also have additional options which can be configured by clicking "Options" on the integration. 42 | 43 | ### Location Integrations 44 | 45 | - You can set the update interval in seconds for polling. 46 | - You can specify the radius (degrees) for tracking satellites above (default 90). 47 | 48 | ### Satellite Integrations 49 | 50 | - You can set the update interval in seconds for polling. 51 | - You can specify the minimum number of seconds you want to have for a visible pass. 52 | 53 | # Important Note 54 | 55 | The N2YO API is rate limited to 1000 transactions per hour. Adding a large number of integrations or setting your polling / update interval too low may result in your API key from being suspended or banned. 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Custom Component Tracking Satellites 2 | 3 | Buy me a coffee [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) 4 | 5 | Using the N2YO API, this Home Assistant integration provides visible satellite passes (general) and to add specific satellites for monitoring. 6 | 7 | You will need to register for an API Key at . 8 | 9 | ## Installation 10 | 11 | To install this integration you will need to add as a custom repository in HACS. 12 | 13 | Once installed you will be able to install the integration from the HACS integrations page. 14 | 15 | Restart your Home Assistant to complete the installation. 16 | 17 | ## Configuration 18 | 19 | Go to Configuration -> Integrations and click the plus sign to add a Satellite Tracker integration. Search for Satellite Tracker (N2YO) and click add. 20 | 21 | You will first need to enter your API key. 22 | 23 | Second you will choose the type of integration you want to install: 24 | 25 | ### Location 26 | 27 | A location integration will add a single sensor which will show you the number of satellites which are currently overhead, and in the attributes will list the satellites and their current positions. 28 | 29 | If you select this option, on the next page you will enter: 30 | 31 | - The name of the location you are tracking (default: the name of your Home Assistant instance) 32 | - The category of satellites you want to track (default: all) 33 | - The latitude of the location you want to track above (default: your Home Assistant latitude) 34 | - The longitude of the location you want to track above (default: your Home Assistant longitude) 35 | - The elevation of the location you want to track above (default: your Home Assistant elevation) 36 | 37 | ### Satellite 38 | 39 | A satellite integration will add 5 sensors for the next visible passes and a device tracker sensor to allow you to plot the current location of a specific satellite on your map. 40 | 41 | If you select this option, on the next page you will enter: 42 | 43 | - The name of the satellite you want to track (default: International Space Station (ISS)) 44 | - The NORAD ID (from n2yo.com) you want to track (default: 25544 for the ISS) 45 | - The latitude for visible pass tracking (default: Your Home Assistant latitude) 46 | - The longitude for visible pass tracking (default: Your Home Assistant longitude) 47 | - The elevation for visible pass tracking (default: Your Home Assistant elevation) 48 | 49 | ## Options 50 | 51 | One you have set up an instance of the integration you also have additional options which can be configured by clicking "Options" on the integration. 52 | 53 | ### Location Integrations 54 | 55 | - You can set the update interval in seconds for polling. 56 | - You can specify the radius (degrees) for tracking satellites above (default 90). 57 | 58 | ### Satellite Integrations 59 | 60 | - You can set the update interval in seconds for polling. 61 | - You can specify the minimum number of seconds you want to have for a visible pass. 62 | 63 | # Important Note 64 | 65 | The N2YO API is rate limited to 1000 transactions per hour. Adding a large number of integrations or setting your polling / update interval too low may result in your API key from being suspended or banned. 66 | 67 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Definition and setup of a Binary Sensor for Satellite Trackers in Home Assistant.""" 2 | 3 | import logging 4 | import time 5 | 6 | from homeassistant.util.dt import as_local, utc_from_timestamp 7 | from homeassistant.helpers.update_coordinator import ( 8 | CoordinatorEntity, 9 | DataUpdateCoordinator, 10 | UpdateFailed, 11 | ) 12 | from homeassistant.components.binary_sensor import BinarySensorEntity 13 | from homeassistant.const import ATTR_NAME 14 | from homeassistant.components.sensor import ENTITY_ID_FORMAT 15 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 16 | 17 | from . import N2YOSatelliteCoordinator 18 | from .const import ( 19 | ATTR_IDENTIFIERS, 20 | ATTR_MANUFACTURER, 21 | ATTR_MODEL, 22 | DOMAIN, 23 | COORDINATOR, 24 | CONF_MIN_ALERT, 25 | DEFAULT_MIN_ALERT, 26 | ) 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | 31 | async def async_setup_entry(hass, entry, async_add_entities): 32 | """Set up the binary sensor platforms.""" 33 | 34 | coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] 35 | sensors = [] 36 | conf = entry.data 37 | min_alert = entry.options.get(CONF_MIN_ALERT, DEFAULT_MIN_ALERT) 38 | 39 | sensors.append( 40 | SatellitePassSensor( 41 | coordinator, 42 | latitude=conf["latitude"], 43 | longitude=conf["longitude"], 44 | elevation=conf["elevation"], 45 | pass_type="Visual", 46 | min_alert=min_alert, 47 | ) 48 | ) 49 | 50 | sensors.append( 51 | SatellitePassSensor( 52 | coordinator, 53 | latitude=conf["latitude"], 54 | longitude=conf["longitude"], 55 | elevation=conf["elevation"], 56 | pass_type="Radio", 57 | min_alert=min_alert, 58 | ) 59 | ) 60 | 61 | async_add_entities(sensors) 62 | 63 | 64 | class SatellitePassSensor(CoordinatorEntity, BinarySensorEntity): 65 | """Defines a Satellite Pass Warning Binary sensor.""" 66 | 67 | def __init__( 68 | self, 69 | coordinator: N2YOSatelliteCoordinator, 70 | latitude: float, 71 | longitude: float, 72 | elevation: float, 73 | pass_type: str, 74 | min_alert: int, 75 | ): 76 | """Initialize Entities.""" 77 | 78 | super().__init__(coordinator=coordinator) 79 | 80 | if pass_type == "Visual": 81 | unique_id = f"{self.coordinator._satellite}_{latitude}_{longitude}_{elevation}" 82 | else: 83 | unique_id = f"{self.coordinator._satellite}_radio_{latitude}_{longitude}_{elevation}" 84 | 85 | self._name = f"{self.coordinator._name} 10 Minute {pass_type} Pass Warning" 86 | self._unique_id = unique_id 87 | self._state = None 88 | self._min_alert = min_alert 89 | self._type = pass_type 90 | self.attrs = {} 91 | 92 | @property 93 | def available(self) -> bool: 94 | """Return if entity is available.""" 95 | return self.coordinator.last_update_success 96 | 97 | @property 98 | def unique_id(self): 99 | """Return the unique Home Assistant friendly identifier for this entity.""" 100 | return self._unique_id 101 | 102 | @property 103 | def name(self): 104 | """Return the friendly name of this entity.""" 105 | return self._name 106 | 107 | @property 108 | def icon(self): 109 | """Return the icon for this entity.""" 110 | return "mdi:satellite-variant" 111 | 112 | @property 113 | def extra_state_attributes(self): 114 | """Return the attributes.""" 115 | return self.attrs 116 | 117 | @property 118 | def device_info(self): 119 | """Define the device based on device_identifier.""" 120 | 121 | device_name = self.coordinator._name 122 | device_model = "Satellite Sensor" 123 | 124 | return { 125 | ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator._satellite)}, 126 | ATTR_NAME: device_name, 127 | ATTR_MANUFACTURER: "N2YO.com", 128 | ATTR_MODEL: device_model, 129 | } 130 | 131 | @property 132 | def is_on(self) -> bool: 133 | """Return the state.""" 134 | next_pass = None 135 | if self._type == "Visual" and self.coordinator.data["visual_passes"]: 136 | next_pass = self.coordinator.data["visual_passes"][0] 137 | elif self._type == "Radio" and self.coordinator.data["radio_passes"]: 138 | next_pass = self.coordinator.data["radio_passes"][0] 139 | 140 | if next_pass: 141 | if next_pass["startUTC"] < ( 142 | time.time() + (10 * 60) 143 | ) and next_pass["startUTC"] > ( 144 | time.time() 145 | ) and next_pass["maxEl"] > self._min_alert: 146 | return True 147 | 148 | return False 149 | 150 | 151 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/__init__.py: -------------------------------------------------------------------------------- 1 | """The Satellite Tracker integration.""" 2 | import asyncio 3 | from datetime import timedelta 4 | import logging 5 | 6 | from n2yoasync import N2YO, N2YOSatelliteCategory 7 | import voluptuous as vol 8 | 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.core import HomeAssistant 11 | from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady 12 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 13 | from homeassistant.helpers import aiohttp_client 14 | from homeassistant.const import ( 15 | CONF_API_KEY, 16 | CONF_LATITUDE, 17 | CONF_LONGITUDE, 18 | CONF_SCAN_INTERVAL, 19 | CONF_ELEVATION, 20 | CONF_TYPE, 21 | CONF_RADIUS, 22 | CONF_NAME, 23 | ) 24 | 25 | from .const import ( 26 | COORDINATOR, 27 | DOMAIN, 28 | SATELLITE_API, 29 | DEFAULT_POLLING_INTERVAL, 30 | TRACKER_TYPE, 31 | DEFAULT_MIN_VISIBILITY, 32 | CONF_MIN_VISIBILITY, 33 | DEFAULT_MIN_ELEVATION, 34 | CONF_MIN_ELEVATION, 35 | CONF_SATELLITE, 36 | ) 37 | 38 | CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) 39 | _LOGGER = logging.getLogger(__name__) 40 | 41 | LOCATION_PLATFORMS = ["sensor"] 42 | SATELLITE_PLATFORMS = ["sensor", "device_tracker", "binary_sensor"] 43 | 44 | async def async_setup(hass: HomeAssistant, config: dict): 45 | """Set up the satellite tracker component.""" 46 | hass.data.setdefault(DOMAIN, {}) 47 | 48 | return True 49 | 50 | 51 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 52 | """Set up satellite tracker from a config entry.""" 53 | conf = entry.data 54 | options = entry.options 55 | 56 | apikey = conf[CONF_API_KEY] 57 | latitude = conf[CONF_LATITUDE] 58 | longitude = conf[CONF_LONGITUDE] 59 | altitude = conf[CONF_ELEVATION] 60 | tracker_type = conf[CONF_TYPE] 61 | name = conf[CONF_NAME] 62 | 63 | polling_interval = options.get(CONF_SCAN_INTERVAL,DEFAULT_POLLING_INTERVAL) 64 | 65 | session = aiohttp_client.async_get_clientsession(hass) 66 | 67 | api = N2YO( 68 | apikey=apikey, 69 | latitude=latitude, 70 | longitude=longitude, 71 | altitude=altitude, 72 | session=session 73 | ) 74 | 75 | try: 76 | if tracker_type == "location": 77 | radius = options.get(CONF_RADIUS,90) 78 | category = conf.get("category",N2YOSatelliteCategory.All) 79 | 80 | await api.get_above(search_radius=radius, category_id=category) 81 | 82 | coordinator = N2YOLocationCoordinator( 83 | hass, 84 | api=api, 85 | name=name, 86 | polling_interval=polling_interval, 87 | tracker_type=tracker_type, 88 | radius=radius, 89 | category=category, 90 | ) 91 | else: 92 | satellite = conf[CONF_SATELLITE] 93 | min_visibility = options.get(CONF_MIN_VISIBILITY,DEFAULT_MIN_VISIBILITY) 94 | min_elevation = options.get(CONF_MIN_ELEVATION,DEFAULT_MIN_ELEVATION) 95 | await api.get_TLE(id=satellite) 96 | 97 | coordinator = N2YOSatelliteCoordinator( 98 | hass, 99 | api=api, 100 | name=name, 101 | polling_interval=polling_interval, 102 | tracker_type=tracker_type, 103 | satellite=satellite, 104 | min_visibility=min_visibility, 105 | min_elevation=min_elevation, 106 | ) 107 | except ConnectionError as error: 108 | _LOGGER.debug("N2YO API Error: %s", error) 109 | raise UpdateFailed from error 110 | except ValueError as error: 111 | _LOGGER.debug("N2YO API Error: %s", error) 112 | raise ConfigEntryNotReady from error 113 | 114 | await coordinator.async_refresh() 115 | 116 | if not coordinator.last_update_success: 117 | raise ConfigEntryNotReady 118 | 119 | hass.data[DOMAIN][entry.entry_id] = { 120 | COORDINATOR: coordinator, 121 | SATELLITE_API: api, 122 | CONF_TYPE: tracker_type 123 | } 124 | 125 | if tracker_type == "location": 126 | platforms = LOCATION_PLATFORMS 127 | else: 128 | platforms = SATELLITE_PLATFORMS 129 | 130 | await hass.config_entries.async_forward_entry_setups(entry, platforms) 131 | 132 | return True 133 | 134 | 135 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 136 | """Unload a config entry.""" 137 | if entry.data[CONF_TYPE] == "location": 138 | platforms = LOCATION_PLATFORMS 139 | else: 140 | platforms = SATELLITE_PLATFORMS 141 | 142 | unload_ok = all( 143 | await asyncio.gather( 144 | *[ 145 | hass.config_entries.async_forward_entry_unload(entry, component) 146 | for component in platforms 147 | ] 148 | ) 149 | ) 150 | if unload_ok: 151 | hass.data[DOMAIN].pop(entry.entry_id) 152 | 153 | return unload_ok 154 | 155 | class N2YOUpdateCoordinator(DataUpdateCoordinator): 156 | """Class to manage fetching update data from the N2YO endpoint.""" 157 | 158 | def __init__( 159 | self, 160 | hass: HomeAssistant, 161 | api: str, 162 | name: str, 163 | polling_interval: int, 164 | tracker_type:str, 165 | ): 166 | """Initialize the global satellite tracker data updater.""" 167 | self.api = api 168 | self._type = tracker_type 169 | self._name = name 170 | 171 | super().__init__( 172 | hass = hass, 173 | logger = _LOGGER, 174 | name = name, 175 | update_interval = timedelta(seconds=polling_interval), 176 | ) 177 | 178 | 179 | class N2YOLocationCoordinator(N2YOUpdateCoordinator): 180 | """Class to manage Location or Category type tracker updates.""" 181 | 182 | def __init__( 183 | self, 184 | hass: HomeAssistant, 185 | api:str, 186 | name:str, 187 | polling_interval:int, 188 | tracker_type:str, 189 | radius:int, 190 | category:int, 191 | ): 192 | """Initialize the location type sensors.""" 193 | 194 | self.radius = radius 195 | self.category = category 196 | 197 | super().__init__( 198 | hass=hass, 199 | api=api, 200 | name=name, 201 | polling_interval=polling_interval, 202 | tracker_type=tracker_type, 203 | ) 204 | 205 | async def _async_update_data(self): 206 | """Fetch data from N2YO for location type data.""" 207 | try: 208 | _LOGGER.debug("Updating the coordinator data.") 209 | update_data = await self.api.get_above( 210 | search_radius=self.radius, 211 | category_id=self.category, 212 | ) 213 | 214 | return update_data 215 | except ConnectionError as error: 216 | _LOGGER.info("N2YO API: %s", error) 217 | raise PlatformNotReady from error 218 | except ValueError as error: 219 | _LOGGER.info("N2YO API: %s", error) 220 | raise UpdateFailed from error 221 | 222 | class N2YOSatelliteCoordinator(N2YOUpdateCoordinator): 223 | """Class to manage Satellite type tracker updates.""" 224 | 225 | def __init__( 226 | self, 227 | hass: HomeAssistant, 228 | api:str, 229 | name:str, 230 | polling_interval:int, 231 | tracker_type:str, 232 | satellite:int, 233 | min_visibility:int, 234 | min_elevation:int, 235 | ): 236 | """Initialize the satellite type data.""" 237 | 238 | self._satellite=satellite 239 | self._min_visibility=min_visibility 240 | self._min_elevation=min_elevation 241 | 242 | super().__init__( 243 | hass=hass, 244 | api=api, 245 | name=name, 246 | polling_interval=polling_interval, 247 | tracker_type=tracker_type, 248 | ) 249 | 250 | async def _async_update_data(self): 251 | """Fetch data from N2YO for location type sensors.""" 252 | try: 253 | _LOGGER.debug("Updating the coordinator data.") 254 | positions_data = await self.api.get_positions( 255 | id=self._satellite, 256 | seconds=1, 257 | ) 258 | visual_passes_data = await self.api.get_visualpasses( 259 | id=self._satellite, 260 | days=10, 261 | min_visibility=self._min_visibility, 262 | ) 263 | radio_passes_data = await self.api.get_radiopasses( 264 | id=self._satellite, 265 | days=10, 266 | min_elevation=self._min_elevation, 267 | ) 268 | 269 | visual_passes = [] 270 | 271 | for this_pass in visual_passes_data: 272 | if this_pass["duration"] > self._min_visibility: 273 | visual_passes.append(this_pass) 274 | 275 | return { 276 | "positions":positions_data, 277 | "visual_passes":visual_passes, 278 | "radio_passes":radio_passes_data, 279 | } 280 | 281 | except ConnectionError as error: 282 | _LOGGER.info("N2YO API: %s", error) 283 | raise PlatformNotReady from error 284 | except ValueError as error: 285 | _LOGGER.info("N2YO API: %s", error) 286 | raise UpdateFailed from error 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/sensor.py: -------------------------------------------------------------------------------- 1 | """Definition and setup of the Satellite Tracker Sensors for Home Assistant.""" 2 | 3 | import logging 4 | import time 5 | import datetime 6 | 7 | from homeassistant.util.dt import as_local, utc_from_timestamp 8 | from homeassistant.components.sensor import ENTITY_ID_FORMAT 9 | from homeassistant.const import UnitOfTime, ATTR_NAME 10 | from homeassistant.helpers.entity import Entity 11 | from homeassistant.helpers.update_coordinator import ( 12 | CoordinatorEntity, 13 | DataUpdateCoordinator, 14 | UpdateFailed, 15 | ) 16 | from . import N2YOLocationCoordinator, N2YOSatelliteCoordinator 17 | 18 | from .const import ( 19 | ATTR_IDENTIFIERS, 20 | ATTR_MANUFACTURER, 21 | ATTR_MODEL, 22 | DOMAIN, 23 | COORDINATOR, 24 | CONF_MIN_VISIBILITY, 25 | DEFAULT_MIN_VISIBILITY, 26 | ) 27 | 28 | _LOGGER = logging.getLogger(__name__) 29 | 30 | async def async_setup_entry(hass, entry, async_add_entities): 31 | """Set up the sensor platforms.""" 32 | 33 | coordinator = hass.data[DOMAIN][entry.entry_id][COORDINATOR] 34 | sensors = [] 35 | 36 | if coordinator._type == "location": 37 | sensors.append( 38 | LocationSensor( 39 | coordinator=coordinator, 40 | icon="mdi:satellite-uplink", 41 | latitude=entry.data["latitude"], 42 | longitude=entry.data["longitude"], 43 | elevation=entry.data["elevation"], 44 | ) 45 | ) 46 | else: 47 | for visible_sensor in range(0,5): 48 | sensors.append( 49 | SatelliteSensor( 50 | coordinator=coordinator, 51 | icon="mdi:satellite-variant", 52 | sat_count=visible_sensor, 53 | pass_type="Visual", 54 | ) 55 | ) 56 | 57 | for radio_sensor in range(0,5): 58 | sensors.append( 59 | SatelliteSensor( 60 | coordinator=coordinator, 61 | icon="mdi:satellite-variant", 62 | sat_count=radio_sensor, 63 | pass_type="Radio", 64 | ) 65 | ) 66 | 67 | async_add_entities(sensors) 68 | 69 | class SatelliteSensor(CoordinatorEntity): 70 | """Defines a Satellite Tracker sensor.""" 71 | 72 | def __init__( 73 | self, 74 | coordinator: N2YOSatelliteCoordinator, 75 | icon: str, 76 | sat_count: int, 77 | pass_type: str, 78 | ): 79 | """Initialize entities.""" 80 | super().__init__(coordinator=coordinator) 81 | 82 | if pass_type == "Visual": 83 | unique_id = f"{coordinator._satellite}_pass_{sat_count}" 84 | else: 85 | unique_id = f"{coordinator._satellite}_radio_pass_{sat_count}" 86 | 87 | self._name = f"{coordinator._name} {pass_type} Pass {sat_count}" 88 | self._unique_id = unique_id 89 | self._state = None 90 | self._icon = icon 91 | self._sat_count = sat_count 92 | self._type = pass_type 93 | self.attrs = {} 94 | 95 | @property 96 | def unique_id(self): 97 | """Return the unique ID for this entity.""" 98 | return self._unique_id 99 | 100 | @property 101 | def name(self): 102 | """Return the name for this entity.""" 103 | return self._name 104 | 105 | @property 106 | def icon(self): 107 | """Return the icon for this entity.""" 108 | return self._icon 109 | 110 | @property 111 | def available(self) -> bool: 112 | """Return if entity is available.""" 113 | return self.coordinator.last_update_success 114 | 115 | @property 116 | def unit_of_measurement(self): 117 | """Return the UoM for this entity.""" 118 | return UnitOfTime.SECONDS 119 | 120 | @property 121 | def state(self): 122 | """Return the state for this entity.""" 123 | state = None 124 | 125 | if self._type == "Visual" and len(self.coordinator.data["visual_passes"]) > self._sat_count: 126 | pass_data = self.coordinator.data["visual_passes"][self._sat_count] 127 | state = pass_data["duration"] 128 | elif self._type == "Radio" and len(self.coordinator.data["radio_passes"]) > self._sat_count: 129 | pass_data = self.coordinator.data["radio_passes"][self._sat_count] 130 | state = pass_data["endUTC"] - pass_data["startUTC"] 131 | 132 | return state 133 | 134 | @property 135 | def extra_state_attributes(self): 136 | """Return the attributes for this entity.""" 137 | self.attrs = {} 138 | if self._type == "Visual" and len(self.coordinator.data["visual_passes"]) > self._sat_count: 139 | pass_data = self.coordinator.data["visual_passes"][self._sat_count] 140 | if pass_data["maxEl"] > 65: 141 | self.attrs["quality"] = "High" 142 | elif pass_data["maxEl"] > 45: 143 | self.attrs["quality"] = "Moderate" 144 | else: 145 | self.attrs["quality"] = "Low" 146 | 147 | self.attrs["max_elevation"] = pass_data["maxEl"] 148 | self.attrs["pass_start"] = as_local(utc_from_timestamp( 149 | pass_data["startUTC"] 150 | )).strftime("%d-%b-%Y %I:%M %p") 151 | self.attrs["pass_start_unix"] = pass_data["startUTC"] 152 | self.attrs["pass_peak"] = as_local(utc_from_timestamp( 153 | pass_data["maxUTC"] 154 | )).strftime("%d-%b-%Y %I:%M %p") 155 | self.attrs["pass_peak_unix"] = pass_data["maxUTC"] 156 | self.attrs["pass_end"] = as_local(utc_from_timestamp( 157 | pass_data["endUTC"] 158 | )).strftime("%d-%b-%Y %I:%M %p") 159 | self.attrs["pass_end_unix"] = pass_data["endUTC"] 160 | self.attrs["start_compass"] = pass_data["startAzCompass"] 161 | self.attrs["end_compass"] = pass_data["endAzCompass"] 162 | 163 | elif self._type == "Radio" and len(self.coordinator.data["radio_passes"]) > self._sat_count: 164 | pass_data = self.coordinator.data["radio_passes"][self._sat_count] 165 | self.attrs["start_compass"] = pass_data["startAzCompass"] 166 | self.attrs["max_compass"] = pass_data["maxAzCompass"] 167 | self.attrs["end_compass"] = pass_data["endAzCompass"] 168 | self.attrs["max_elevation"] = pass_data["maxEl"] 169 | self.attrs["start_azimuth"] = pass_data["startAz"] 170 | self.attrs["max_azimuth"] = pass_data["maxAz"] 171 | self.attrs["end_azimuth"] = pass_data["endAz"] 172 | self.attrs["duration"] = pass_data["startUTC"] - pass_data["endUTC"] 173 | self.attrs["start"] = as_local(utc_from_timestamp( 174 | pass_data["startUTC"] 175 | )).strftime("%d-%b-%Y %I:%M %p") 176 | self.attrs["end"] = as_local(utc_from_timestamp( 177 | pass_data["endUTC"] 178 | )).strftime("%d-%b-%Y %I:%M %p") 179 | self.attrs["start_unix"] = pass_data["startUTC"] 180 | self.attrs["end_unix"] = pass_data["endUTC"] 181 | 182 | return self.attrs 183 | 184 | @property 185 | def device_info(self): 186 | """Define the device based on device_identifier.""" 187 | 188 | device_name = self.coordinator._name 189 | device_model = "Satellite Sensor" 190 | 191 | return { 192 | ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator._satellite)}, 193 | ATTR_NAME: device_name, 194 | ATTR_MANUFACTURER: "N2YO.com", 195 | ATTR_MODEL: device_model, 196 | } 197 | 198 | 199 | 200 | class LocationSensor(CoordinatorEntity): 201 | """Defines a location tracker sensor.""" 202 | def __init__( 203 | self, 204 | coordinator: N2YOLocationCoordinator, 205 | icon: str, 206 | latitude: float, 207 | longitude: float, 208 | elevation: float, 209 | ): 210 | """Initialize entities.""" 211 | super().__init__(coordinator=coordinator) 212 | 213 | self._name = f"{coordinator._name} Overhead Satellites" 214 | self._unique_id = f"{coordinator._name}_{latitude}_{longitude}_{elevation}" 215 | self._state = None 216 | self._icon = icon 217 | self.attrs = {} 218 | 219 | @property 220 | def unique_id(self): 221 | """Return the unique ID for this entity.""" 222 | return self._unique_id 223 | 224 | @property 225 | def name(self): 226 | """Return the name of this entity.""" 227 | return self._name 228 | 229 | @property 230 | def available(self) -> bool: 231 | """Return if entity is available.""" 232 | return self.coordinator.last_update_success 233 | 234 | @property 235 | def icon(self): 236 | """Return the icon for this sensor.""" 237 | return self._icon 238 | 239 | @property 240 | def state(self): 241 | """Return the state for this entity.""" 242 | return len(self.coordinator.data) 243 | 244 | @property 245 | def extra_state_attributes(self): 246 | """Return the state attributes for this entity.""" 247 | self.attrs = {} 248 | 249 | sat_count = 1 250 | for satellite in self.coordinator.data: 251 | self.attrs[f"sat_{sat_count}_id"] = satellite["satid"] 252 | self.attrs[f"sat_{sat_count}_name"] = satellite["satname"] 253 | self.attrs[f"sat_{sat_count}_launch_date"] = satellite["launchDate"] 254 | self.attrs[f"sat_{sat_count}_latitude"] = satellite["satlat"] 255 | self.attrs[f"sat_{sat_count}_longitude"] = satellite["satlng"] 256 | self.attrs[f"sat_{sat_count}_elevation"] = satellite["satalt"] 257 | sat_count += 1 258 | 259 | return self.attrs 260 | 261 | @property 262 | def unit_of_measurement(self): 263 | """Return the UoM for this entity.""" 264 | return "satellites" 265 | 266 | @property 267 | def device_info(self): 268 | """Define the device based on device_identifier.""" 269 | 270 | device_name = self.coordinator._name 271 | device_model = "Location Sensor" 272 | 273 | return { 274 | ATTR_IDENTIFIERS: {(DOMAIN, device_name)}, 275 | ATTR_NAME: device_name, 276 | ATTR_MANUFACTURER: "N2YO.com", 277 | ATTR_MODEL: device_model, 278 | } 279 | 280 | -------------------------------------------------------------------------------- /custom_components/satellitetracker/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Satellite Tracker integration.""" 2 | from n2yoasync import N2YO, N2YOSatelliteCategory, AuthenticationError 3 | import voluptuous as vol 4 | import aiohttp 5 | import logging 6 | 7 | from homeassistant import config_entries 8 | from homeassistant.helpers import config_entry_flow 9 | from homeassistant.helpers import aiohttp_client 10 | from homeassistant.core import callback 11 | from homeassistant.const import ( 12 | CONF_API_KEY, 13 | CONF_LATITUDE, 14 | CONF_LONGITUDE, 15 | CONF_SCAN_INTERVAL, 16 | CONF_ELEVATION, 17 | CONF_TYPE, 18 | CONF_RADIUS, 19 | CONF_NAME, 20 | ) 21 | 22 | from .const import ( 23 | DOMAIN, 24 | CONF_MIN_VISIBILITY, 25 | DEFAULT_MIN_VISIBILITY, 26 | CONF_MIN_ELEVATION, 27 | DEFAULT_MIN_ELEVATION, 28 | DEFAULT_POLLING_INTERVAL, 29 | CONF_SATELLITE, 30 | CONF_MIN_ALERT, 31 | DEFAULT_MIN_ALERT, 32 | ) 33 | 34 | _LOGGER = logging.getLogger(__name__) 35 | 36 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 37 | """Handle a config flow for N2YO Satellite Tracker.""" 38 | 39 | VERSION = 1 40 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 41 | 42 | @staticmethod 43 | @callback 44 | def async_get_options_flow(config_entry): 45 | """Get the options flow for this handler.""" 46 | return OptionsFlowHandler(config_entry) 47 | 48 | async def async_step_user(self, user_input=None): 49 | """Handle the initial step.""" 50 | errors = {} 51 | 52 | if user_input is not None: 53 | session = aiohttp_client.async_get_clientsession(self.hass) 54 | key = user_input[CONF_API_KEY] 55 | api_client = N2YO(apikey=key,session=session) 56 | 57 | try: 58 | await api_client.get_TLE(id=25544) 59 | except AuthenticationError: 60 | errors["base"] = "invalid_auth" 61 | except aiohttp.ClientError: 62 | errors["base"] = "cannot_connect" 63 | except Exception: # pylint: disable=broad-except 64 | _LOGGER.exception("Unexpected exception") 65 | errors["base"] = "unknown" 66 | else: 67 | self.apikey = key 68 | self.session = session 69 | self.sensortype = None 70 | self.longitude = None 71 | self.latitude = None 72 | self.elevation = None 73 | self.name = None 74 | 75 | return await self.async_step_type() 76 | 77 | return self.async_show_form( 78 | step_id="user", 79 | data_schema=vol.Schema( 80 | { 81 | vol.Required(CONF_API_KEY): str, 82 | } 83 | ), 84 | errors=errors, 85 | ) 86 | 87 | async def async_step_type(self, user_input=None): 88 | """Select the type of sensor to create.""" 89 | if user_input is not None: 90 | if user_input[CONF_TYPE] == "location": 91 | self.sensortype = "location" 92 | return await self.async_step_location() 93 | else: 94 | self.sensortype = "satellite" 95 | return await self.async_step_satellite() 96 | 97 | return self.async_show_form( 98 | step_id="type", 99 | data_schema=vol.Schema( 100 | { 101 | vol.Required(CONF_TYPE):vol.In({ 102 | "location":"Location", 103 | "satellite":"Satellite", 104 | }) 105 | } 106 | ) 107 | ) 108 | 109 | async def async_step_location(self, user_input=None): 110 | """Enter the appropriate location data.""" 111 | categories = {} 112 | 113 | for name, member in N2YOSatelliteCategory.__members__.items(): 114 | categories[member.value] = name 115 | 116 | if user_input is not None: 117 | self.name = user_input[CONF_NAME] 118 | self.latitude = user_input[CONF_LATITUDE] 119 | self.longitude = user_input[CONF_LONGITUDE] 120 | self.elevation = user_input[CONF_ELEVATION] 121 | self.category = user_input["category"] 122 | 123 | user_data = { 124 | CONF_NAME:self.name, 125 | CONF_LATITUDE:self.latitude, 126 | CONF_LONGITUDE:self.longitude, 127 | CONF_ELEVATION:self.elevation, 128 | "category":self.category, 129 | CONF_API_KEY:self.apikey, 130 | CONF_TYPE:self.sensortype, 131 | } 132 | 133 | unique_id = f"{self.latitude}_{self.longitude}_{self.elevation}_{self.category}" 134 | await self.async_set_unique_id(unique_id) 135 | self._abort_if_unique_id_configured() 136 | return self.async_create_entry(title=self.name, data=user_data) 137 | 138 | 139 | return self.async_show_form( 140 | step_id="location", 141 | data_schema=vol.Schema( 142 | { 143 | vol.Required(CONF_NAME, default=self.hass.config.location_name):str, 144 | vol.Required("category", default=0):vol.In(categories), 145 | vol.Required( 146 | CONF_LATITUDE, 147 | default=self.hass.config.latitude, 148 | ):vol.All( 149 | vol.Coerce(float), 150 | vol.Range(min=-90,max=90), 151 | ), 152 | vol.Required( 153 | CONF_LONGITUDE, 154 | default=self.hass.config.longitude, 155 | ):vol.All( 156 | vol.Coerce(float), 157 | vol.Range(min=-180,max=180), 158 | ), 159 | vol.Required( 160 | CONF_ELEVATION, 161 | default=self.hass.config.elevation, 162 | ):vol.Coerce(float), 163 | } 164 | ) 165 | ) 166 | 167 | async def async_step_satellite(self, user_input=None): 168 | """Enter the appropriate location data.""" 169 | if user_input is not None: 170 | self.name = user_input[CONF_NAME] 171 | self.latitude = user_input[CONF_LATITUDE] 172 | self.longitude = user_input[CONF_LONGITUDE] 173 | self.elevation = user_input[CONF_ELEVATION] 174 | self.satellite = user_input[CONF_SATELLITE] 175 | 176 | user_data = { 177 | CONF_NAME:self.name, 178 | CONF_LATITUDE:self.latitude, 179 | CONF_LONGITUDE:self.longitude, 180 | CONF_ELEVATION:self.elevation, 181 | CONF_SATELLITE:self.satellite, 182 | CONF_API_KEY:self.apikey, 183 | CONF_TYPE:self.sensortype, 184 | } 185 | 186 | unique_id = f"{self.satellite}_{self.latitude}_{self.longitude}_{self.elevation}" 187 | await self.async_set_unique_id(unique_id) 188 | self._abort_if_unique_id_configured() 189 | return self.async_create_entry(title=self.name, data=user_data) 190 | 191 | return self.async_show_form( 192 | step_id="satellite", 193 | data_schema=vol.Schema( 194 | { 195 | vol.Required(CONF_NAME, default="International Space Station (ISS)"):str, 196 | vol.Required(CONF_SATELLITE, default=25544):int, 197 | vol.Required( 198 | CONF_LATITUDE, 199 | default=self.hass.config.latitude, 200 | ):vol.All( 201 | vol.Coerce(float), 202 | vol.Range(min=-90,max=90), 203 | ), 204 | vol.Required( 205 | CONF_LONGITUDE, 206 | default=self.hass.config.longitude, 207 | ):vol.All( 208 | vol.Coerce(float), 209 | vol.Range(min=-180,max=180), 210 | ), 211 | vol.Required( 212 | CONF_ELEVATION, 213 | default=self.hass.config.elevation, 214 | ):vol.Coerce(float), 215 | } 216 | ) 217 | ) 218 | 219 | class OptionsFlowHandler(config_entries.OptionsFlow): 220 | """Handle satellite tracker client options.""" 221 | 222 | def __init__(self, config_entry): 223 | """Initialize options flow.""" 224 | self._config_entry = config_entry 225 | 226 | async def async_step_init(self, user_input=None): 227 | """Manage options.""" 228 | 229 | if user_input is not None: 230 | return self.async_create_entry(title="", data=user_input) 231 | 232 | if self._config_entry.data[CONF_TYPE] == "location": 233 | return self.async_show_form( 234 | step_id="init", 235 | data_schema=vol.Schema( 236 | { 237 | vol.Optional( 238 | CONF_SCAN_INTERVAL, 239 | default=self._config_entry.options.get( 240 | CONF_SCAN_INTERVAL, DEFAULT_POLLING_INTERVAL 241 | ) 242 | ):int, 243 | vol.Optional( 244 | CONF_RADIUS, 245 | default=self._config_entry.options.get( 246 | CONF_RADIUS, 90 247 | ) 248 | ):int, 249 | } 250 | ), 251 | ) 252 | else: 253 | return self.async_show_form( 254 | step_id="init", 255 | data_schema=vol.Schema( 256 | { 257 | vol.Optional( 258 | CONF_SCAN_INTERVAL, 259 | default=self._config_entry.options.get( 260 | CONF_SCAN_INTERVAL, DEFAULT_POLLING_INTERVAL 261 | ), 262 | ):int, 263 | vol.Optional( 264 | CONF_MIN_VISIBILITY, 265 | default=self._config_entry.options.get( 266 | CONF_MIN_VISIBILITY, DEFAULT_MIN_VISIBILITY 267 | ), 268 | ):int, 269 | vol.Optional( 270 | CONF_MIN_ELEVATION, 271 | default=self._config_entry.options.get( 272 | CONF_MIN_ELEVATION, DEFAULT_MIN_ELEVATION 273 | ), 274 | ):int, 275 | vol.Optional( 276 | CONF_MIN_ALERT, 277 | default=self._config_entry.options.get( 278 | CONF_MIN_ALERT, DEFAULT_MIN_ALERT 279 | ), 280 | ):int, 281 | } 282 | ), 283 | ) 284 | 285 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------