├── .github ├── FUNDING.yml └── workflows │ ├── debian.yml │ ├── python-publish_to_pypi.yml │ └── python-test_and_lint.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs ├── az-ccso-sar.jpg ├── gba-inreach-la-50%.png ├── gba-inreach-la.png └── inrcot-conop.png ├── example-config.ini ├── inrcot ├── __init__.py ├── classes.py ├── commands.py ├── constants.py └── functions.py ├── requirements.txt ├── requirements_test.txt ├── setup.py └── tests ├── data ├── bad-data.kml ├── bad-data2.kml ├── bad.kml ├── test-config.ini └── test.kml └── test_functions.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ampledata 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: ampledata 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: https://www.buymeacoffee.com/ampledata 14 | -------------------------------------------------------------------------------- /.github/workflows/debian.yml: -------------------------------------------------------------------------------- 1 | name: Build Debian package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | DEB_BUILD_OPTIONS: nocheck 10 | 11 | jobs: 12 | build-artifact: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@master 17 | 18 | - name: Install packaging dependencies 19 | run: | 20 | sudo apt-get update -qq 21 | sudo apt-get install -y \ 22 | python3 python3-dev python3-pip python3-venv python3-all \ 23 | dh-python debhelper devscripts dput software-properties-common \ 24 | python3-distutils python3-setuptools python3-wheel python3-stdeb 25 | 26 | - name: Build Debian/Apt sdist_dsc 27 | run: | 28 | rm -Rf deb_dist/* 29 | python3 setup.py --command-packages=stdeb.command sdist_dsc 30 | 31 | - name: Build Debian/Apt bdist_deb 32 | run: | 33 | export REPO_NAME=$(echo ${{ github.repository }} | awk -F"/" '{print $2}') 34 | python3 setup.py --command-packages=stdeb.command bdist_deb 35 | ls -al deb_dist/ 36 | cp deb_dist/python3-${REPO_NAME}_*_all.deb deb_dist/python3-${REPO_NAME}_latest_all.deb 37 | 38 | - uses: actions/upload-artifact@master 39 | with: 40 | name: artifact-deb 41 | path: | 42 | deb_dist/*.deb 43 | 44 | - name: Create Release 45 | id: create_release 46 | uses: actions/create-release@master 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | tag_name: ${{ github.ref }} 51 | release_name: Release ${{ github.ref }} 52 | draft: false 53 | prerelease: false 54 | 55 | - name: Upload Release Asset 56 | id: upload-release-asset 57 | uses: svenstaro/upload-release-action@v2 58 | with: 59 | repo_token: ${{ secrets.GITHUB_TOKEN }} 60 | file: deb_dist/*.deb 61 | tag: ${{ github.ref }} 62 | overwrite: true 63 | file_glob: true -------------------------------------------------------------------------------- /.github/workflows/python-publish_to_pypi.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | name: Publish package to PyPI 3 | 4 | on: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | deploy: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: '3.x' 20 | - name: Install dependencies 21 | run: | 22 | python3 -m pip install --upgrade pip 23 | python3 -m pip install setuptools wheel twine 24 | - name: Build 25 | run: | 26 | python3 setup.py sdist bdist_wheel 27 | - name: Publish package 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | with: 30 | user: __token__ 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/python-test_and_lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test Code 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-20.04 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install test requirements 25 | run: | 26 | make install_test_requirements 27 | - name: Install package itself (editable) 28 | run: | 29 | make editable 30 | - name: Lint with pylint 31 | run: | 32 | make pylint 33 | - name: Lint with flake8 34 | run: | 35 | make flake8 36 | - name: Test with pytest-cov 37 | run: | 38 | make test_cov 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.deb 3 | *.egg 4 | *.egg-info/ 5 | *.egg/ 6 | *.ignore 7 | *.py[co] 8 | *.py[oc] 9 | *.spl 10 | *.vagrant 11 | .DS_Store 12 | .coverage 13 | .eggs/ 14 | .eggs/* 15 | .idea 16 | .idea/ 17 | .pt 18 | .vagrant/ 19 | RELEASE-VERSION.txt 20 | build/ 21 | cover/ 22 | dist/ 23 | dump.rdb 24 | flake8.log 25 | local/ 26 | local_* 27 | metadata/ 28 | nosetests.xml 29 | output.xml 30 | pylint.log 31 | redis-server.log 32 | redis-server/ 33 | __pycache__ 34 | .ipynb_checkpoints/ 35 | config.ini 36 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | INRCOT 5.2.0 2 | ------------ 3 | Major updates. 4 | - Updated copyrights, etc. 5 | - Documentation & Readme updates. 6 | - Now supports Data Package server configuration. 7 | - Fixed custom user icon support. 8 | - Added unit tests. 9 | - Now requires PyTAK >= 5.6.1. 10 | - Refactoring, style improvements, linting, black. 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Greg Albrecht 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE requirements.txt 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 Greg Albrecht 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Author:: Greg Albrecht 17 | # 18 | 19 | this_app = inrcot 20 | .DEFAULT_GOAL := all 21 | 22 | all: editable 23 | 24 | develop: 25 | python3 setup.py develop 26 | 27 | editable: 28 | python3 -m pip install -e . 29 | 30 | install_test_requirements: 31 | python3 -m pip install -r requirements_test.txt 32 | 33 | install: 34 | python3 setup.py install 35 | 36 | uninstall: 37 | python3 -m pip uninstall -y $(this_app) 38 | 39 | reinstall: uninstall install 40 | 41 | publish: 42 | python3 setup.py publish 43 | 44 | clean: 45 | @rm -rf *.egg* build dist *.py[oc] */*.py[co] cover doctest_pypi.cfg \ 46 | nosetests.xml pylint.log output.xml flake8.log tests.log \ 47 | test-result.xml htmlcov fab.log .coverage __pycache__ \ 48 | */__pycache__ 49 | 50 | pep8: 51 | flake8 --max-line-length=88 --extend-ignore=E203,E231 --exit-zero $(this_app)/*.py 52 | 53 | flake8: pep8 54 | 55 | lint: 56 | pylint --msg-template="{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}" \ 57 | --max-line-length=88 -r n $(this_app)/*.py || exit 0 58 | 59 | pylint: lint 60 | 61 | checkmetadata: 62 | python3 setup.py check -s --restructuredtext 63 | 64 | mypy: 65 | mypy --strict . 66 | 67 | pytest: 68 | pytest 69 | 70 | test: editable install_test_requirements pytest 71 | 72 | test_cov: 73 | pytest --cov=$(this_app) --cov-report term-missing 74 | 75 | black: 76 | black . 77 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Garmin inReach to Cursor on Target Gateway 2 | ****************************************** 3 | 4 | .. image:: https://raw.githubusercontent.com/ampledata/inrcot/main/docs/az-ccso-sar.jpg 5 | :alt: Screenshot of INRCOT being used on a Search & Rescue mission in Arizona. 6 | :target: https://raw.githubusercontent.com/ampledata/inrcot/main/docs/az-ccso-sar.jpg 7 | 8 | * Pictured: Screenshot of INRCOT being used on a Search & Rescue mission in Arizona. 9 | 10 | The inReach to Cursor on Target Gateway (INRCOT) transforms Garmin inReach 11 | position messages into Cursor on Target (CoT) for display on TAK Products such as 12 | ATAK, WinTAK, iTAK, et al. Single or multi-device feeds are supported. 13 | 14 | Other situational awareness products, including as RaptorX, TAKX & COPERS have been 15 | tested. 16 | 17 | INRCOT requires a `Garmin inReach `_ 18 | device with service. 19 | 20 | .. image:: https://raw.githubusercontent.com/ampledata/inrcot/main/docs/inrcot-conop.png 21 | :alt: Diagram of INRCOT's Concept of Operations (CONOP). 22 | :target: https://raw.githubusercontent.com/ampledata/inrcot/main/docs/inrcot-conop.png 23 | 24 | * Pictured: Diagram of INRCOT's Concept of Operations (CONOP). 25 | 26 | 27 | Support Development 28 | =================== 29 | 30 | .. image:: https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png 31 | :target: https://www.buymeacoffee.com/ampledata 32 | :alt: Support Development: Buy me a coffee! 33 | 34 | 35 | Use Cases 36 | ========= 37 | 38 | There are numerous applications for satellite based position location information, 39 | including: 40 | 41 | 1. Wildland fire unit tracking 42 | 2. Blue Force Tracking 43 | 3. Search & Rescue (SAR) 44 | 4. Partner Forces PLI 45 | 5. Asset Tracking 46 | 6. Data diode, CDS & cybersecurity considerations 47 | 48 | See also Section 1114.d of the `Dingell Act `_:: 49 | 50 | Location Systems for Wildland Firefighters.-- 51 | (1) In general.--Not later than 2 years after the date of 52 | enactment of this Act, subject to the availability of 53 | appropriations, the Secretaries, in coordination with State 54 | wildland firefighting agencies, shall jointly develop and 55 | operate a tracking system (referred to in this subsection as the 56 | ``system'') to remotely locate the positions of fire resources 57 | for use by wildland firefighters, including, at a minimum, any 58 | fire resources assigned to Federal type 1 wildland fire incident 59 | management teams. 60 | 61 | 62 | Requirements 63 | ============ 64 | 65 | INRCOT uses the Garmin Explore "MapShare" feature. 66 | 67 | 1. Login to Garmin Explore: https://explore.garmin.com/ 68 | 2. Browse to the "MY INFO" page: https://explore.garmin.com/Inbox 69 | 3. Click "Social". 70 | 4. Under MapShare > Enable MapShare click to enable 'MapShare: On'. 71 | 5. Click "Feeds" and note the "Raw KML Data" URL, we'll use this URL, write it down. 72 | 73 | For more information on inReach KML Feeds see: https://support.garmin.com/en-US/?faq=tdlDCyo1fJ5UxjUbA9rMY8 74 | 75 | 76 | Install 77 | ======= 78 | 79 | INRCOT functionality is provided via a command-line tool named ``inrcot``. 80 | To install ``inrcot``: 81 | 82 | Debian, Ubuntu, Raspbian, Raspberry OS:: 83 | 84 | $ sudo apt update 85 | $ wget https://github.com/ampledata/pytak/releases/latest/download/python3-pytak_latest_all.deb 86 | $ sudo apt install -f ./python3-pytak_latest_all.deb 87 | $ wget https://github.com/ampledata/inrcot/releases/latest/download/python3-inrcot_latest_all.deb 88 | $ sudo apt install -f ./python3-inrcot_latest_all.deb 89 | 90 | CentOS, et al:: 91 | 92 | $ sudo python3 -m pip install inrcot 93 | 94 | Install from source:: 95 | 96 | $ git clone https://github.com/ampledata/inrcot.git 97 | $ cd inrcot/ 98 | $ python3 setup.py install 99 | 100 | 101 | Usage 102 | ===== 103 | 104 | The ``inrcot`` program has two command-line arguments:: 105 | 106 | $ inrcot -h 107 | usage: inrcot [-h] [-c CONFIG_FILE] [-p PREF_PACKAGE] 108 | 109 | optional arguments: 110 | -h, --help show this help message and exit 111 | -c CONFIG_FILE, --CONFIG_FILE CONFIG_FILE 112 | Optional configuration file. Default: config.ini 113 | -p PREF_PACKAGE, --PREF_PACKAGE PREF_PACKAGE 114 | Optional connection preferences package zip file (aka data package). 115 | 116 | 117 | Configuration 118 | ============= 119 | 120 | Configuration parameters can be specified either via environment variables or in 121 | a INI-stile configuration file. An example configuration file, click here for an 122 | example configuration file `example-config.ini `_. 123 | 124 | Global Config Parameters: 125 | 126 | * **POLL_INTERVAL**: How many seconds between checking for new messages at the Spot API? Default: ``120`` (seconds). 127 | * **COT_STALE**: How many seconds until CoT is stale? Default: ``600`` (seconds) 128 | * **COT_TYPE**: CoT Type. Default: ``a-f-g-e-s`` 129 | 130 | For each feed (1 inReach = 1 feed, multiple feeds supported), these config params can be set: 131 | 132 | * **FEED_URL**: URL to the MapShare KML. 133 | * **COT_STALE**: How many seconds until CoT is stale? Default: ``600`` (seconds) 134 | * **COT_TYPE**: CoT Type. Default: ``a-f-g-e-s`` 135 | * **COT_NAME**: CoT Callsign. Defaults to the MapShare KML Placemark name. 136 | * **COT_ICON**: CoT User Icon. If set, will set the CoT ``usericon`` element, for use with custom TAK icon sets. 137 | * **FEED_USERNAME**: MapShare username, for use with protected MapShare. 138 | * **FEED_PASSWORD**: MapShare password, for use with protected MapShare. 139 | 140 | TLS & other configuration parameters available via `PyTAK `_. 141 | 142 | 143 | Example Configurations 144 | ====================== 145 | 146 | An example config:: 147 | 148 | [inrcot] 149 | COT_URL = tcp://takserver.example.com:8088 150 | POLL_INTERVAL = 120 151 | 152 | [inrcot_feed_aaa] 153 | FEED_URL = https://share.garmin.com/Feed/Share/aaa 154 | 155 | Multiple feeds can be added by creating multiple `inrcot_feed` sections:: 156 | 157 | [inrcot] 158 | COT_URL = tcp://takserver.example.com:8088 159 | POLL_INTERVAL = 120 160 | 161 | [inrcot_feed_xxx] 162 | FEED_URL = https://share.garmin.com/Feed/Share/xxx 163 | 164 | [inrcot_feed_yyy] 165 | FEED_URL = https://share.garmin.com/Feed/Share/yyy 166 | 167 | Individual feeds CoT output can be customized as well:: 168 | 169 | [inrcot] 170 | COT_URL = tcp://takserver.example.com:8088 171 | POLL_INTERVAL = 120 172 | 173 | [inrcot_feed_zzz] 174 | FEED_URL = https://share.garmin.com/Feed/Share/zzz 175 | COT_TYPE = a-f-G-U-C 176 | COT_STALE = 600 177 | COT_NAME = Team Lead 178 | COT_ICON = my_package/team_lead.png 179 | 180 | Protected feeds are also supported:: 181 | 182 | [inrcot] 183 | COT_URL = tcp://takserver.example.com:8088 184 | POLL_INTERVAL = 120 185 | 186 | [inrcot_feed_ppp] 187 | FEED_URL = https://share.garmin.com/Feed/Share/ppp 188 | FEED_USERNAME = secretsquirrel 189 | FEED_PASSWORD = supersecret 190 | 191 | 192 | 193 | Source 194 | ====== 195 | INRCOT Source can be found on Github: https://github.com/ampledata/inrcot 196 | 197 | 198 | Author 199 | ====== 200 | INRCOT is written and maintained by Greg Albrecht W2GMD oss@undef.net 201 | 202 | https://ampledata.org/ 203 | 204 | 205 | Copyright 206 | ========= 207 | INRCOT is Copyright 2023 Greg Albrecht 208 | 209 | 210 | License 211 | ======= 212 | Copyright 2023 Greg Albrecht 213 | 214 | Licensed under the Apache License, Version 2.0 (the "License"); 215 | you may not use this file except in compliance with the License. 216 | You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 217 | 218 | Unless required by applicable law or agreed to in writing, software 219 | distributed under the License is distributed on an "AS IS" BASIS, 220 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 221 | See the License for the specific language governing permissions and 222 | limitations under the License. 223 | -------------------------------------------------------------------------------- /docs/az-ccso-sar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snstac/inrcot/5693b3dc5417c2736630c70924aae362da57fb67/docs/az-ccso-sar.jpg -------------------------------------------------------------------------------- /docs/gba-inreach-la-50%.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snstac/inrcot/5693b3dc5417c2736630c70924aae362da57fb67/docs/gba-inreach-la-50%.png -------------------------------------------------------------------------------- /docs/gba-inreach-la.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snstac/inrcot/5693b3dc5417c2736630c70924aae362da57fb67/docs/gba-inreach-la.png -------------------------------------------------------------------------------- /docs/inrcot-conop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snstac/inrcot/5693b3dc5417c2736630c70924aae362da57fb67/docs/inrcot-conop.png -------------------------------------------------------------------------------- /example-config.ini: -------------------------------------------------------------------------------- 1 | [inrcot] 2 | ; ^-- Always make sure to include this section header somewhere. 3 | 4 | COT_URL = udp://239.2.3.1:6969 5 | 6 | POLL_INTERVAL = 120 7 | 8 | [inrcot_feed_1] 9 | FEED_URL = https://share.garmin.com/Feed/Share/ampledata 10 | COT_TYPE = a-f-G-U-C 11 | COT_STALE = 600 12 | 13 | -------------------------------------------------------------------------------- /inrcot/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | """inReach to Cursor on Target Gateway. 19 | 20 | :author: Greg Albrecht 21 | :copyright: Copyright 2023 Greg Albrecht 22 | :license: Apache License, Version 2.0 23 | :source: 24 | """ 25 | 26 | from .constants import DEFAULT_POLL_INTERVAL, DEFAULT_COT_STALE, DEFAULT_COT_TYPE 27 | 28 | from .functions import create_tasks, inreach_to_cot, split_feed, create_feeds 29 | 30 | from .classes import Worker 31 | 32 | __author__ = "Greg Albrecht " 33 | __copyright__ = "Copyright 2023 Greg Albrecht" 34 | __license__ = "Apache License, Version 2.0" 35 | -------------------------------------------------------------------------------- /inrcot/classes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | """INRCOT Class Definitions.""" 19 | 20 | import asyncio 21 | 22 | from typing import Optional 23 | 24 | import aiohttp 25 | 26 | import pytak 27 | import inrcot 28 | 29 | 30 | __author__ = "Greg Albrecht " 31 | __copyright__ = "Copyright 2023 Greg Albrecht" 32 | __license__ = "Apache License, Version 2.0" 33 | 34 | 35 | class Worker(pytak.QueueWorker): 36 | """Read inReach Feed, renders to CoT, and puts on a TX queue.""" 37 | 38 | def __init__(self, queue: asyncio.Queue, config, orig_config) -> None: 39 | super().__init__(queue, config) 40 | self.inreach_feeds: list = inrcot.create_feeds(orig_config) 41 | 42 | async def handle_data(self, data: bytes, feed_conf: dict) -> None: 43 | """Handle the response from the inReach API.""" 44 | feeds: Optional[list] = inrcot.split_feed(data) 45 | if not feeds: 46 | return None 47 | for feed in feeds: 48 | event: Optional[bytes] = inrcot.inreach_to_cot(feed, feed_conf) 49 | if not event: 50 | self._logger.debug("Empty CoT Event") 51 | continue 52 | await self.put_queue(event) 53 | 54 | async def get_inreach_feeds(self) -> None: 55 | """Get inReach Feed from API.""" 56 | for feed_conf in self.inreach_feeds: 57 | feed_auth = feed_conf.get("feed_auth") 58 | if not feed_auth: 59 | self._logger.warning("No feed_auth specified.") 60 | continue 61 | 62 | feed_url = feed_conf.get("feed_url") 63 | if not feed_url: 64 | self._logger.warning("No feed_url specified.") 65 | continue 66 | 67 | async with aiohttp.ClientSession() as session: 68 | try: 69 | response = await session.request( 70 | method="GET", auth=feed_auth, url=feed_url 71 | ) 72 | except Exception as exc: # NOQA pylint: disable=broad-except 73 | self._logger.warning("Exception raised while polling inReach API.") 74 | self._logger.exception(exc) 75 | continue 76 | 77 | status: int = response.status 78 | if status != 200: 79 | self._logger.warning( 80 | "No valid response from inReach API: status=%s", status 81 | ) 82 | self._logger.debug(response) 83 | continue 84 | 85 | await self.handle_data(await response.content.read(), feed_conf) 86 | 87 | async def run(self, number_of_iterations=-1) -> None: 88 | """Run this Worker, Reads from Pollers.""" 89 | self._logger.info("Run: %s", self.__class__) 90 | 91 | poll_interval: int = int( 92 | self.config.get("POLL_INTERVAL", inrcot.DEFAULT_POLL_INTERVAL) 93 | ) 94 | 95 | while 1: 96 | await self.get_inreach_feeds() 97 | await asyncio.sleep(poll_interval) 98 | -------------------------------------------------------------------------------- /inrcot/commands.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """PyTAK Command Line.""" 18 | 19 | import argparse 20 | import asyncio 21 | import importlib 22 | import logging 23 | import os 24 | import platform 25 | import pprint 26 | import sys 27 | import warnings 28 | 29 | from configparser import ConfigParser, SectionProxy 30 | 31 | import pytak 32 | 33 | # Python 3.6 support: 34 | if sys.version_info[:2] >= (3, 7): 35 | from asyncio import get_running_loop 36 | else: 37 | warnings.warn("Using Python < 3.7, consider upgrading Python.") 38 | from asyncio import get_event_loop as get_running_loop 39 | 40 | __author__ = "Greg Albrecht " 41 | __copyright__ = "Copyright 2023 Greg Albrecht" 42 | __license__ = "Apache License, Version 2.0" 43 | 44 | 45 | async def main( 46 | app_name: str, config: SectionProxy, original_config: ConfigParser 47 | ) -> None: 48 | """ 49 | Abstract implementation of an async main function. 50 | 51 | Parameters 52 | ---------- 53 | app_name : `str` 54 | Name of the app calling this function. 55 | config : `SectionProxy` 56 | A dict of configuration parameters & values. 57 | """ 58 | app = importlib.__import__(app_name) 59 | clitool: pytak.CLITool = pytak.CLITool(config) 60 | create_tasks = getattr(app, "create_tasks") 61 | await clitool.setup() 62 | clitool.add_tasks(create_tasks(config, clitool, original_config)) 63 | await clitool.run() 64 | 65 | 66 | def cli(app_name: str = "inrcot") -> None: 67 | """ 68 | Abstract implementation of a Command Line Interface (CLI). 69 | 70 | Parameters 71 | ---------- 72 | app_name : `str` 73 | Name of the app calling this function. 74 | """ 75 | app = importlib.__import__(app_name) 76 | 77 | parser = argparse.ArgumentParser() 78 | parser.add_argument( 79 | "-c", 80 | "--CONFIG_FILE", 81 | dest="CONFIG_FILE", 82 | default="config.ini", 83 | type=str, 84 | help="Optional configuration file. Default: config.ini", 85 | ) 86 | parser.add_argument( 87 | "-p", 88 | "--PREF_PACKAGE", 89 | dest="PREF_PACKAGE", 90 | required=False, 91 | type=str, 92 | help="Optional connection preferences package zip file (aka data package).", 93 | ) 94 | namespace = parser.parse_args() 95 | cli_args = {k: v for k, v in vars(namespace).items() if v is not None} 96 | 97 | # Read config: 98 | env_vars = os.environ 99 | 100 | # Remove env vars that contain '%', which ConfigParser or pprint barf on: 101 | env_vars = {key: val for key, val in env_vars.items() if "%" not in val} 102 | 103 | env_vars["COT_URL"] = env_vars.get("COT_URL", pytak.DEFAULT_COT_URL) 104 | env_vars["COT_HOST_ID"] = f"{app_name}@{platform.node()}" 105 | env_vars["COT_STALE"] = getattr(app, "DEFAULT_COT_STALE", pytak.DEFAULT_COT_STALE) 106 | 107 | orig_config: ConfigParser = ConfigParser(env_vars) 108 | 109 | config_file = cli_args.get("CONFIG_FILE") 110 | if config_file and os.path.exists(config_file): 111 | logging.info("Reading configuration from %s", config_file) 112 | orig_config.read(config_file) 113 | else: 114 | orig_config.add_section(app_name) 115 | 116 | config: SectionProxy = orig_config[app_name] 117 | 118 | pref_package: str = config.get("PREF_PACKAGE", cli_args.get("PREF_PACKAGE")) 119 | if pref_package and os.path.exists(pref_package): 120 | pref_config = pytak.read_pref_package(pref_package) 121 | config.update(pref_config) 122 | 123 | debug = config.getboolean("DEBUG") 124 | if debug: 125 | print(f"Showing Config: {config_file}") 126 | print("=" * 10) 127 | pprint.pprint(dict(config)) 128 | print("=" * 10) 129 | 130 | if sys.version_info[:2] >= (3, 7): 131 | asyncio.run(main(app_name, config, orig_config), debug=debug) 132 | else: 133 | loop = get_running_loop() 134 | try: 135 | loop.run_until_complete(main(app_name, config, orig_config)) 136 | finally: 137 | loop.close() 138 | -------------------------------------------------------------------------------- /inrcot/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """INRCOT Constants.""" 18 | 19 | __author__ = "Greg Albrecht " 20 | __copyright__ = "Copyright 2023 Greg Albrecht" 21 | __license__ = "Apache License, Version 2.0" 22 | 23 | 24 | # How long between checking for new messages at the Spot API? 25 | DEFAULT_POLL_INTERVAL: int = 120 26 | 27 | # How longer after producting the CoT Event is the Event 'stale' (seconds) 28 | DEFAULT_COT_STALE: str = "600" 29 | 30 | # Default CoT type. 'a-f-g-e-s' works in iTAK, WinTAK & ATAK... 31 | DEFAULT_COT_TYPE: str = "a-f-g-e-s" 32 | -------------------------------------------------------------------------------- /inrcot/functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """INRCOT Gateway Functions.""" 18 | 19 | import datetime 20 | import io 21 | import xml.etree.ElementTree as ET 22 | 23 | from configparser import ConfigParser 24 | from typing import Optional, Set 25 | 26 | from aiohttp import BasicAuth 27 | 28 | import pytak 29 | import inrcot 30 | 31 | 32 | __author__ = "Greg Albrecht " 33 | __copyright__ = "Copyright 2023 Greg Albrecht" 34 | __license__ = "Apache License, Version 2.0" 35 | 36 | 37 | def create_tasks( 38 | config: ConfigParser, clitool: pytak.CLITool, original_config: ConfigParser 39 | ) -> Set[pytak.Worker,]: 40 | """Create specific coroutine task set for this application. 41 | 42 | Parameters 43 | ---------- 44 | config : `ConfigParser` 45 | Configuration options & values. 46 | clitool : `pytak.CLITool` 47 | A PyTAK Worker class instance. 48 | 49 | Returns 50 | ------- 51 | `set` 52 | Set of PyTAK Worker classes for this application. 53 | """ 54 | return set([inrcot.Worker(clitool.tx_queue, config, original_config)]) 55 | 56 | 57 | def split_feed(content: bytes) -> Optional[list]: 58 | """Split an inReach MapShare KML feed by 'Folder'.""" 59 | tree = ET.parse(io.BytesIO(content)) 60 | document = tree.find("{http://www.opengis.net/kml/2.2}Document") 61 | if not document: 62 | return None 63 | folder = document.findall("{http://www.opengis.net/kml/2.2}Folder") 64 | return folder 65 | 66 | 67 | def make_feed_conf(section) -> dict: 68 | """Make a feed conf dictionary from a conf.""" 69 | feed_conf: dict = { 70 | "feed_url": section.get("FEED_URL"), 71 | "cot_stale": section.get("COT_STALE", inrcot.DEFAULT_COT_STALE), 72 | "cot_type": section.get("COT_TYPE", inrcot.DEFAULT_COT_TYPE), 73 | "cot_icon": section.get("COT_ICON"), 74 | "cot_name": section.get("COT_NAME"), 75 | } 76 | # Support "private" MapShare feeds: 77 | feed_pass: str = section.get("FEED_PASSWORD") 78 | feed_user: str = section.get("FEED_USERNAME") 79 | if feed_pass and feed_user: 80 | feed_auth: BasicAuth = BasicAuth(feed_user, feed_pass) 81 | feed_conf["feed_auth"] = str(feed_auth) 82 | 83 | return feed_conf 84 | 85 | 86 | def create_feeds(config: ConfigParser) -> list: 87 | """Create a list of feed configurations.""" 88 | feeds: list = [] 89 | for feed in config.sections(): 90 | if not "inrcot_feed_" in feed: 91 | continue 92 | config_section = config[feed] 93 | feed_conf: dict = make_feed_conf(config_section) 94 | feed_conf["feed_name"] = feed 95 | feeds.append(feed_conf) 96 | return feeds 97 | 98 | 99 | def inreach_to_cot_xml( 100 | feed: str, feed_conf: Optional[dict] = None 101 | ) -> Optional[ET.Element]: 102 | """Convert an inReach Response to a Cursor-on-Target Event, as an XML Obj.""" 103 | feed_conf = feed_conf or {} 104 | 105 | placemarks = feed.find("{http://www.opengis.net/kml/2.2}Placemark") 106 | _point = placemarks.find("{http://www.opengis.net/kml/2.2}Point") 107 | coordinates = _point.find("{http://www.opengis.net/kml/2.2}coordinates").text 108 | _name = placemarks.find("{http://www.opengis.net/kml/2.2}name").text 109 | 110 | ts = placemarks.find("{http://www.opengis.net/kml/2.2}TimeStamp") 111 | when = ts.find("{http://www.opengis.net/kml/2.2}when").text 112 | 113 | if not "," in coordinates or coordinates.count(",") != 2: 114 | return None 115 | 116 | lon, lat, alt = coordinates.split(",") 117 | if not all([lat, lon]): 118 | return None 119 | 120 | time = when 121 | 122 | # We want to use localtime + stale instead of lastUpdate time + stale 123 | # This means a device could go offline and we might not know it? 124 | _cot_stale = feed_conf.get("cot_stale", inrcot.DEFAULT_COT_STALE) 125 | cot_stale = ( 126 | datetime.datetime.now(datetime.timezone.utc) 127 | + datetime.timedelta(seconds=int(_cot_stale)) 128 | ).strftime(pytak.ISO_8601_UTC) 129 | 130 | cot_type = feed_conf.get("cot_type", inrcot.DEFAULT_COT_TYPE) 131 | 132 | name = feed_conf.get("cot_name") or _name 133 | callsign = name 134 | 135 | point = ET.Element("point") 136 | point.set("lat", str(lat)) 137 | point.set("lon", str(lon)) 138 | point.set("hae", "9999999.0") 139 | point.set("ce", "9999999.0") 140 | point.set("le", "9999999.0") 141 | 142 | contact = ET.Element("contact") 143 | contact.set("callsign", f"{callsign} (inReach)") 144 | 145 | detail = ET.Element("detail") 146 | detail.append(contact) 147 | 148 | remarks = ET.Element("remarks") 149 | 150 | _remarks = f"Garmin inReach User.\r\n Name: {name}" 151 | 152 | detail.set("remarks", _remarks) 153 | remarks.text = _remarks 154 | detail.append(remarks) 155 | 156 | cot_icon: Optional[str] = feed_conf.get("cot_icon") 157 | if cot_icon: 158 | usericon = ET.Element("usericon") 159 | usericon.set("iconsetpath", cot_icon) 160 | detail.append(usericon) 161 | 162 | root = ET.Element("event") 163 | root.set("version", "2.0") 164 | root.set("type", cot_type) 165 | root.set("uid", f"Garmin-inReach.{name}".replace(" ", "")) 166 | root.set("how", "m-g") 167 | root.set("time", time) # .strftime(pytak.ISO_8601_UTC)) 168 | root.set("start", time) # .strftime(pytak.ISO_8601_UTC)) 169 | root.set("stale", cot_stale) 170 | root.append(point) 171 | root.append(detail) 172 | 173 | return root 174 | 175 | 176 | def inreach_to_cot(content: str, feed_conf: Optional[dict] = None) -> Optional[bytes]: 177 | """Render a CoT XML as a string.""" 178 | cot: Optional[ET.Element] = inreach_to_cot_xml(content, feed_conf) 179 | return ET.tostring(cot) if cot else None 180 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Python Distribution Package Requirements -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-asyncio 2 | pytest-cov 3 | pylint 4 | flake8 5 | black 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2023 Greg Albrecht 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | """Setup for the inReach Cursor-on-Target Gateway. 18 | 19 | :author: Greg Albrecht 20 | :copyright: Copyright 2023 Greg Albrecht 21 | :license: Apache License, Version 2.0 22 | :source: https://github.com/ampledata/inrcot 23 | """ 24 | 25 | import os 26 | import sys 27 | 28 | import setuptools 29 | 30 | __title__ = "inrcot" 31 | __version__ = "5.2.0" 32 | __author__ = "Greg Albrecht " 33 | __copyright__ = "Copyright 2023 Greg Albrecht" 34 | __license__ = "Apache License, Version 2.0" 35 | 36 | 37 | def publish(): 38 | """Publish this package to pypi.""" 39 | if sys.argv[-1] == "publish": 40 | os.system("python setup.py sdist") 41 | os.system("twine upload dist/*") 42 | sys.exit() 43 | 44 | 45 | publish() 46 | 47 | 48 | def read_readme(readme_file="README.rst") -> str: 49 | """Read the contents of the README file for use as a long_description.""" 50 | readme: str = "" 51 | this_directory = os.path.abspath(os.path.dirname(__file__)) 52 | with open(os.path.join(this_directory, readme_file), encoding="UTF-8") as rmf: 53 | readme = rmf.read() 54 | return readme 55 | 56 | 57 | setuptools.setup( 58 | version=__version__, 59 | name=__title__, 60 | packages=[__title__], 61 | package_dir={__title__: __title__}, 62 | url=f"https://github.com/ampledata/{__title__}", 63 | entry_points={"console_scripts": [f"{__title__} = {__title__}.commands:cli"]}, 64 | description="inReach Cursor on Target Gateway.", 65 | author="Greg Albrecht", 66 | author_email="oss@undef.net", 67 | package_data={"": ["LICENSE"]}, 68 | license="Apache License, Version 2.0", 69 | long_description=read_readme(), 70 | long_description_content_type="text/x-rst", 71 | zip_safe=False, 72 | include_package_data=True, 73 | install_requires=["pytak >= 5.6.1", "aiohttp"], 74 | classifiers=[ 75 | "Programming Language :: Python :: 3", 76 | "License :: OSI Approved :: Apache Software License", 77 | "Operating System :: OS Independent", 78 | ], 79 | keywords=[ 80 | "Satellite", 81 | "SAR", 82 | "Search and Rescue", 83 | "Cursor on Target", 84 | "ATAK", 85 | "TAK", 86 | "CoT", 87 | "WinTAK", 88 | "iTAK", 89 | "TAK Server", 90 | ], 91 | ) 92 | -------------------------------------------------------------------------------- /tests/data/bad-data.kml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KML Export 8/30/2021 5:13:22 PM 5 | 16 | 28 | 40 | 48 | 49 | Greg Albrecht 50 | 51 | Greg Albrecht 52 | true 53 | 54 | 55 | 2021-07-22T15:22:30Z 56 | 57 | #style_1658884 58 | 59 | 60 | 207049997 61 | 62 | 63 | 7/22/2021 3:22:30 PM 64 | 65 | 66 | 7/22/2021 8:22:30 AM 67 | 68 | 69 | Greg Albrecht 70 | 71 | 72 | Greg Albrecht 73 | 74 | 75 | inReach Mini 76 | 77 | 78 | 300434033719020 79 | 80 | 81 | 82 | 83 | 84 | 33.874926 85 | 86 | 87 | -118.346915 88 | 89 | 90 | 22.63 m from MSL 91 | 92 | 93 | 0.0 km/h 94 | 95 | 96 | 0.00 ° True 97 | 98 | 99 | True 100 | 101 | 102 | False 103 | 104 | 105 | 106 | 107 | 108 | Tracking interval received. 109 | 110 | 111 | 112 | 113 | 114 | WGS84 115 | 116 | 117 | 118 | false 119 | absolute 120 | -118.346915,,22.63 121 | 122 | 123 | 124 | Greg Albrecht 125 | true 126 | Greg Albrecht's track log 127 | #linestyle_1658884 128 | 129 | true 130 | -118.346915,33.874926,22.63 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tests/data/bad-data2.kml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KML Export 8/30/2021 5:13:22 PM 5 | 16 | 28 | 40 | 48 | 49 | Greg Albrecht 50 | 51 | Greg Albrecht 52 | true 53 | 54 | 55 | 2021-07-22T15:22:30Z 56 | 57 | #style_1658884 58 | 59 | 60 | 207049997 61 | 62 | 63 | 7/22/2021 3:22:30 PM 64 | 65 | 66 | 7/22/2021 8:22:30 AM 67 | 68 | 69 | Greg Albrecht 70 | 71 | 72 | Greg Albrecht 73 | 74 | 75 | inReach Mini 76 | 77 | 78 | 300434033719020 79 | 80 | 81 | 82 | 83 | 84 | 33.874926 85 | 86 | 87 | -118.346915 88 | 89 | 90 | 22.63 m from MSL 91 | 92 | 93 | 0.0 km/h 94 | 95 | 96 | 0.00 ° True 97 | 98 | 99 | True 100 | 101 | 102 | False 103 | 104 | 105 | 106 | 107 | 108 | Tracking interval received. 109 | 110 | 111 | 112 | 113 | 114 | WGS84 115 | 116 | 117 | 118 | false 119 | absolute 120 | -118.346915,22.63 121 | 122 | 123 | 124 | Greg Albrecht 125 | true 126 | Greg Albrecht's track log 127 | #linestyle_1658884 128 | 129 | true 130 | -118.346915,33.874926,22.63 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tests/data/bad.kml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KML Export 8/30/2021 5:13:22 PM 5 | 16 | 28 | 40 | 48 | 49 | Greg Albrecht 50 | 51 | Greg Albrecht 52 | true 53 | 54 | 55 | 2021-07-22T15:22:30Z 56 | 57 | #style_1658884 58 | 59 | 60 | 207049997 61 | 62 | 63 | 7/22/2021 3:22:30 PM 64 | 65 | 66 | 7/22/2021 8:22:30 AM 67 | 68 | 69 | Greg Albrecht 70 | 71 | 72 | Greg Albrecht 73 | 74 | 75 | inReach Mini 76 | 77 | 78 | 300434033719020 79 | 80 | 81 | 82 | 83 | 84 | 33.874926 85 | 86 | 87 | -118.346915 88 | 89 | 90 | 22.63 m from MSL 91 | 92 | 93 | 0.0 km/h 94 | 95 | 96 | 0.00 ° True 97 | 98 | 99 | True 100 | 101 | 102 | False 103 | 104 | 105 | 106 | 107 | 108 | Tracking interval received. 109 | 110 | 111 | 112 | 113 | 114 | WGS84 115 | 116 | 117 | 118 | false 119 | absolute 120 | -118.346915,33.874926,22.63 121 | 122 | 123 | 124 | Greg Albrecht 125 | true 126 | Greg Albrecht's track log 127 | #linestyle_1658884 128 | 129 | true 130 | -118.346915,33.874926,22.63 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tests/data/test-config.ini: -------------------------------------------------------------------------------- 1 | [inrcot] 2 | 3 | COT_URL = udp://239.2.3.1:6969 4 | 5 | POLL_INTERVAL = 120 6 | 7 | [inrcot_feed_1] 8 | FEED_URL = https://share.garmin.com/Feed/Share/ampledata 9 | COT_TYPE = a-f-G-U-C 10 | COT_STALE = 600 11 | COT_ICON = TACOS/taco.png 12 | 13 | [inrcot_feed_aaa] 14 | FEED_URL = https://share.garmin.com/Feed/Share/aaa 15 | 16 | [inrcot_feed_xxx] 17 | FEED_URL = https://share.garmin.com/Feed/Share/xxx 18 | 19 | [inrcot_feed_yyy] 20 | FEED_URL = https://share.garmin.com/Feed/Share/yyy 21 | 22 | [inrcot_feed_zzz] 23 | FEED_URL = https://share.garmin.com/Feed/Share/zzz 24 | COT_TYPE = a-f-G-U-C 25 | COT_STALE = 600 26 | COT_NAME = Team Lead 27 | COT_ICON = my_package/team_lead.png 28 | 29 | [inrcot_feed_ppp] 30 | FEED_URL = https://share.garmin.com/Feed/Share/ppp 31 | FEED_USERNAME = secretsquirrel 32 | FEED_PASSWORD = supersecret -------------------------------------------------------------------------------- /tests/data/test.kml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | KML Export 8/30/2021 5:13:22 PM 5 | 16 | 28 | 40 | 48 | 49 | Greg Albrecht 50 | 51 | Greg Albrecht 52 | true 53 | 54 | 55 | 2021-07-22T15:22:30Z 56 | 57 | #style_1658884 58 | 59 | 60 | 207049997 61 | 62 | 63 | 7/22/2021 3:22:30 PM 64 | 65 | 66 | 7/22/2021 8:22:30 AM 67 | 68 | 69 | Greg Albrecht 70 | 71 | 72 | Greg Albrecht 73 | 74 | 75 | inReach Mini 76 | 77 | 78 | 300434033719020 79 | 80 | 81 | 82 | 83 | 84 | 33.874926 85 | 86 | 87 | -118.346915 88 | 89 | 90 | 22.63 m from MSL 91 | 92 | 93 | 0.0 km/h 94 | 95 | 96 | 0.00 ° True 97 | 98 | 99 | True 100 | 101 | 102 | False 103 | 104 | 105 | 106 | 107 | 108 | Tracking interval received. 109 | 110 | 111 | 112 | 113 | 114 | WGS84 115 | 116 | 117 | 118 | false 119 | absolute 120 | -118.346915,33.874926,22.63 121 | 122 | 123 | 124 | Greg Albrecht 125 | true 126 | Greg Albrecht's track log 127 | #linestyle_1658884 128 | 129 | true 130 | -118.346915,33.874926,22.63 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """inReach to Cursor-on-Target Gateway Function Tests.""" 5 | 6 | from configparser import ConfigParser, SectionProxy 7 | from aiohttp import BasicAuth 8 | 9 | import unittest 10 | 11 | import inrcot.functions 12 | 13 | __author__ = "Greg Albrecht " 14 | __copyright__ = "Copyright 2023 Greg Albrecht" 15 | __license__ = "Apache License, Version 2.0" 16 | 17 | 18 | class FunctionsTestCase(unittest.TestCase): 19 | """Test for inrcot Functions.""" 20 | 21 | def test_inreach_to_cot_xml(self): 22 | """Test rendering inReach KML a Python XML Object.""" 23 | with open("tests/data/test.kml", "rb") as test_kml_fd: 24 | test_kml_feed = test_kml_fd.read() 25 | 26 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 27 | test_cot = inrcot.functions.inreach_to_cot_xml(test_kml, {}) 28 | 29 | point = test_cot.find("point") 30 | 31 | self.assertEqual(test_cot.get("type"), "a-f-g-e-s") 32 | self.assertEqual(test_cot.get("uid"), "Garmin-inReach.GregAlbrecht") 33 | self.assertEqual(point.get("lat"), "33.874926") 34 | self.assertEqual(point.get("lon"), "-118.346915") 35 | 36 | def test_inreach_to_cot(self): 37 | """Test rendering inReach KML as a Python XML String.""" 38 | with open("tests/data/test.kml", "rb") as test_kml_fd: 39 | test_kml_feed = test_kml_fd.read() 40 | 41 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 42 | test_cot = inrcot.functions.inreach_to_cot(test_kml, {}) 43 | 44 | self.assertIn(b"Greg Albrecht (inReach)", test_cot) 45 | 46 | def test_inreach_to_cot_xml_from_config(self): 47 | """Test rendering inReach KML a Python XML Object.""" 48 | with open("tests/data/test.kml", "rb") as test_kml_fd: 49 | test_kml_feed = test_kml_fd.read() 50 | 51 | test_config_file = "tests/data/test-config.ini" 52 | orig_config: ConfigParser = ConfigParser() 53 | orig_config.read(test_config_file) 54 | feeds = inrcot.functions.create_feeds(orig_config) 55 | 56 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 57 | test_cot = inrcot.functions.inreach_to_cot_xml(test_kml, feeds[0]) 58 | 59 | point = test_cot.find("point") 60 | detail = test_cot.find("detail") 61 | usericon = detail.find("usericon") 62 | 63 | self.assertEqual(test_cot.get("type"), "a-f-G-U-C") 64 | self.assertEqual(test_cot.get("uid"), "Garmin-inReach.GregAlbrecht") 65 | self.assertEqual(point.get("lat"), "33.874926") 66 | self.assertEqual(point.get("lon"), "-118.346915") 67 | self.assertEqual(usericon.get("iconsetpath"), "TACOS/taco.png") 68 | 69 | def test_inreach_to_cot_from_config(self): 70 | """Test rendering inReach KML as a Python XML String.""" 71 | with open("tests/data/test.kml", "rb") as test_kml_fd: 72 | test_kml_feed = test_kml_fd.read() 73 | 74 | test_config_file = "tests/data/test-config.ini" 75 | orig_config: ConfigParser = ConfigParser() 76 | orig_config.read(test_config_file) 77 | feeds = inrcot.functions.create_feeds(orig_config) 78 | 79 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 80 | test_cot = inrcot.functions.inreach_to_cot(test_kml, feeds[0]) 81 | 82 | self.assertIn(b"Greg Albrecht (inReach)", test_cot) 83 | 84 | def test_create_feeds(self): 85 | """Test creating feeds from config.""" 86 | test_config_file = "tests/data/test-config.ini" 87 | orig_config: ConfigParser = ConfigParser() 88 | orig_config.read(test_config_file) 89 | 90 | # config: SectionProxy = orig_config["inrcot"] 91 | 92 | feeds = inrcot.functions.create_feeds(orig_config) 93 | self.assertTrue(len(feeds) == 6) 94 | feed = feeds[0] 95 | self.assertEqual(feed["feed_name"], "inrcot_feed_1") 96 | self.assertEqual(feed.get("cot_type"), "a-f-G-U-C") 97 | self.assertEqual(feed["cot_type"], "a-f-G-U-C") 98 | self.assertEqual( 99 | feed["feed_url"], "https://share.garmin.com/Feed/Share/ampledata" 100 | ) 101 | self.assertEqual(feed["cot_stale"], "600") 102 | self.assertEqual(feed["cot_icon"], "TACOS/taco.png") 103 | self.assertEqual(feed["cot_name"], None) 104 | 105 | def test_create_feeds_with_auth(self): 106 | """Test creating feeds with auth from config.""" 107 | test_config_file = "tests/data/test-config.ini" 108 | orig_config: ConfigParser = ConfigParser() 109 | orig_config.read(test_config_file) 110 | 111 | feeds = inrcot.functions.create_feeds(orig_config) 112 | self.assertTrue(len(feeds) == 6) 113 | feed = feeds[5] 114 | self.assertEqual(feed["feed_name"], "inrcot_feed_ppp") 115 | self.assertEqual(feed.get("cot_type"), "a-f-g-e-s") 116 | self.assertEqual(feed["cot_type"], "a-f-g-e-s") 117 | self.assertEqual(feed["feed_url"], "https://share.garmin.com/Feed/Share/ppp") 118 | self.assertEqual(feed["cot_stale"], "600") 119 | self.assertEqual(feed["cot_icon"], None) 120 | self.assertEqual(feed["cot_name"], None) 121 | self.assertEqual( 122 | feed["feed_auth"], 123 | "BasicAuth(login='secretsquirrel', password='supersecret', encoding='latin1')", 124 | ) 125 | 126 | def test_inreach_to_cot_xml_bad_kml(self): 127 | """Test rendering bad KML.""" 128 | with open("tests/data/bad.kml", "rb") as test_kml_fd: 129 | test_kml_feed = test_kml_fd.read() 130 | 131 | test_kml = inrcot.functions.split_feed(test_kml_feed) 132 | self.assertEqual(test_kml, None) 133 | 134 | def test_inreach_to_cot_xml_bad_data(self): 135 | """Test rendering bad KML.""" 136 | with open("tests/data/bad-data.kml", "rb") as test_kml_fd: 137 | test_kml_feed = test_kml_fd.read() 138 | 139 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 140 | test_cot = inrcot.functions.inreach_to_cot(test_kml, {}) 141 | self.assertEqual(test_cot, None) 142 | 143 | def test_inreach_to_cot_xml_bad_data2(self): 144 | """Test rendering bad KML.""" 145 | with open("tests/data/bad-data2.kml", "rb") as test_kml_fd: 146 | test_kml_feed = test_kml_fd.read() 147 | 148 | test_kml = inrcot.functions.split_feed(test_kml_feed)[0] 149 | test_cot = inrcot.functions.inreach_to_cot(test_kml, {}) 150 | self.assertEqual(test_cot, None) 151 | 152 | 153 | if __name__ == "__main__": 154 | unittest.main() 155 | --------------------------------------------------------------------------------