Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ redis-server.log
redis-server/
.python-version
__pycache__
config.ini
private_key.pem
Pipfile
15 changes: 7 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SFPD CAD to Cursor-On-Target Gateway.
PulsePoint to Cursor-On-Target Gateway.
*************************************

.. image:: https://raw.githubusercontent.com/ampledata/pulsecot/main/docs/Screenshot_20201026-142037_ATAK-25p.jpg
Expand All @@ -8,8 +8,7 @@ SFPD CAD to Cursor-On-Target Gateway.
Example agencies:
* DC Fire & EMS: EMS1205

The SFPD CAD to Cursor-On-Target Gateway (SFPDCADCOT) transforms SFPD Computer
Aided Dispatch (CAD) calls for service to Cursor-On-Target (COT) Events for
The PulsePoint to Cursor-On-Target Gateway (PulseCOT) transforms PulsePoint incidents/AEDs to Cursor-On-Target (COT) Events for
display on Situational Awareness applications such as the Android Team
Awareness Kit (ATAK), WinTAK, RaptorX, TAKX, iTAK, et al. More information on
the TAK suite of tools cal be found at: https://www.tak.gov/
Expand All @@ -32,7 +31,7 @@ efforts is greatly appreciated.
Installation
============

SFPDCADCOT's functionality provided by a command-line program called `pulsecot`.
PulseCOT's functionality provided by a command-line program called `pulsecot`.

Installing as a Debian / Ubuntu Package [Recommended]::

Expand Down Expand Up @@ -75,7 +74,7 @@ a INI-stile configuration file.

Parameters:

* **CAD_URL**: (*optional*) SFPD CAD Data URL.
* **AGENCY_IDS**: (*optional*) PulsePoint agency ID for displaying incidents/AEDs.
* **COT_URL**: (*optional*) Destination for Cursor-On-Target messages. See `PyTAK <https://github.com/ampledata/pytak#configuration-parameters>`_ for options.
* **POLL_INTERVAL**: (*optional*) Period in seconds to poll API. Default: 30

Expand All @@ -90,19 +89,19 @@ Configuration parameters are imported in the following priority order:

Source
======
SFPDCADCOT source can be found on Github: https://github.com/ampledata/pulsecot
PulseCOT source can be found on Github: https://github.com/ampledata/pulsecot


Author
======
SFPDCADCOT is written and maintained by Greg Albrecht W2GMD [email protected]
PulseCOT is written and maintained by Greg Albrecht W2GMD [email protected]

https://ampledata.org/


Copyright
=========
SFPDCADCOT is Copyright 2022 Greg Albrecht
PulseCOT is Copyright 2022 Greg Albrecht


License
Expand Down
5 changes: 4 additions & 1 deletion pulsecot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@
DEFAULT_AGENCY_IDS,
PP_CALL_TYPES,
DEFAULT_PP_URL,
DEFAULT_PP_AED_URL,
DEFAULT_PP_AED_API_USERNAME,
DEFAULT_PP_AED_API_PASSWORD
)

from .functions import incident_to_cot, create_tasks # NOQA
from .functions import incident_to_cot, create_tasks, aed_to_cot # NOQA

from .classes import CADWorker # NOQA

Expand Down
53 changes: 39 additions & 14 deletions pulsecot/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,58 @@ class CADWorker(pytak.QueueWorker):

agency_details: dict = {}

async def handle_data(self, data: list) -> None:
async def handle_data(self, data: list, type) -> None:
"""
Transforms Data to COT and puts it onto TX queue.
"""
if not data:
return
if type != "incident":
for aed in data.get('aeds',[]):
event: Union[str, None] = pulsecot.aed_to_cot(
aed, config=self.config, agency=data["agency"]
)
if not event:
self._logger.debug("Empty CoT")
continue
await self.put_queue(event)
else:
for incident in data.get("incidents", []):
event: Union[str, None] = pulsecot.incident_to_cot(
incident, config=self.config, agency=data["agency"]
)

for incident in data.get("incidents", []):
event: Union[str, None] = pulsecot.incident_to_cot(
incident, config=self.config, agency=data["agency"]
)

if not event:
self._logger.debug("Empty CoT")
continue
if not event:
self._logger.debug("Empty CoT")
continue

await self.put_queue(event)
await self.put_queue(event)

async def get_pp_feed(self, agency_id: str) -> dict:
if not agency_id:
self._logger.warning("No agency_id specified, try `find_agency()`?")
return
agency_id = agency_id.replace("\"","")
aed_url: str = f"{pulsecot.DEFAULT_PP_AED_URL}{agency_id}"
async with self.session.get(aed_url, auth=aiohttp.BasicAuth(pulsecot.DEFAULT_PP_AED_API_USERNAME, pulsecot.DEFAULT_PP_AED_API_PASSWORD)) as resp:
if resp.status != 200:
response_content = await resp.text()
self._logger.error("Received HTTP Status %s for %s", resp.status, aed_url)
self._logger.error(response_content)
return
json_resp = await resp.json(content_type="application/json")
if json_resp == None:
return

url: str = f"{pulsecot.DEFAULT_PP_URL}{agency_id}"
async with self.session.get(url) as resp:
aeds: Union[dict, None] = json_resp.get("aeds", {})
if aeds:
data = {"aeds": aeds, "agency": self.agency_details[agency_id]}
await self.handle_data(data, "aeds")
inc_url: str = f"{pulsecot.DEFAULT_PP_URL}{agency_id}"
async with self.session.get(inc_url) as resp:
if resp.status != 200:
response_content = await resp.text()
self._logger.error("Received HTTP Status %s for %s", resp.status, url)
self._logger.error("Received HTTP Status %s for %s", resp.status, inc_url)
self._logger.error(response_content)
return

Expand All @@ -84,7 +108,7 @@ async def get_pp_feed(self, agency_id: str) -> dict:

data = {"incidents": active, "agency": self.agency_details[agency_id]}

await self.handle_data(data)
await self.handle_data(data, "incident")

async def run(self, number_of_iterations=-1) -> None:
"""Runs this Thread, Reads from Pollers."""
Expand All @@ -98,6 +122,7 @@ async def run(self, number_of_iterations=-1) -> None:
# Populate the agency hints:
agencies = pulsecot.gnu.get_agencies()
for agency_id in agency_ids.split(","):
agency_id = agency_id.replace("\"","")
self.agency_details[agency_id] = agencies[agency_id]

async with aiohttp.ClientSession() as self.session:
Expand Down
4 changes: 4 additions & 0 deletions pulsecot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
DEFAULT_COT_STALE: str = "130"
DEFAULT_AGENCY_IDS: str = "21105,EMS1384,41000"
DEFAULT_PP_URL: str = "https://web.pulsepoint.org/DB/giba.php?agency_id="
DEFAULT_PP_AED_URL: str = "https://api.pulsepoint.org/v2/aed?apikey=mSwngLqWvSrQEVWu4eFF62Z2fZgsnCD5ZTaA8ZV&agencyid="
DEFAULT_PP_AED_API_USERNAME: str = "aedviewer"
DEFAULT_PP_AED_API_PASSWORD: str = "1ScOvupxfZ"


DEFAULT_PP_CALL_TYPES_FILE: str = os.getenv(
"PP_CALL_TYPES_FILE",
Expand Down
99 changes: 99 additions & 0 deletions pulsecot/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,96 @@ def create_tasks(
"""
return set([pulsecot.CADWorker(clitool.tx_queue, config)])

def aed_to_cot_xml(
aed: dict,
config: Union[SectionProxy, None] = None,
agency: Union[dict, None] = None,
) -> Union[ET.Element, None]:
"""
Serializes a PulsePoint AEDs as Cursor-On-Target XML.

Parameters
----------
incident : `dict`
Key/Value data struct of PulsePoint Incident.
config : `configparser.SectionProxy`
Configuration options and values.

Returns
-------
`xml.etree.ElementTree.Element`
Cursor-On-Target XML ElementTree object.
"""
config: dict = config or {}
locString = aed['LatitudeLongitude'].split(",")
lat = locString[0]
lon = locString[1]

if lat is None or lon is None:
return None

if lat == "0.0000000000" or lon == "0.0000000000":
return None

remarks_fields = []

pp_id: str = aed["ID"]
cot_stale: int = int(config.get("COT_STALE"))
cot_host_id: str = config.get("COT_HOST_ID", pytak.DEFAULT_HOST_ID)
cot_uid: str = f"PulsePoint-{agency['agency_initials']}-AED-{pp_id}"
cot_type: str = "a-u-G"

callsign = f"AED - {aed['AEDLocationName']}"
iconsetpath = "f7f71666-8b28-4b57-9fbb-e38e61d33b79/Google/earthquake.png"
if aed['AEDIsPrivate'] == True:
remarks_fields.append('Open to Public: No')
else:
remarks_fields.append('Open to Public: Yes')
remarks_fields.append(f"AED Location: {aed['AEDLocationDescription']}")
remarks_fields.append(f"Agency: {agency['short_agencyname']}")

remarks_fields.append(f"PPID: {pp_id}")

point = ET.Element("point")
point.set("lat", str(lat))
point.set("lon", str(lon))

point.set("ce", str("9999999.0"))
point.set("le", str("9999999.0"))
point.set("hae", str("9999999.0"))

contact = ET.Element("contact")
contact.set("callsign", str(callsign))

usericon = ET.Element("usericon")
usericon.set("iconsetpath", iconsetpath)

detail = ET.Element("detail")
detail.append(contact)
detail.append(usericon)

remarks = ET.Element("remarks")

remarks_fields.append(f"{cot_host_id}")

_remarks = "\n".join(list(filter(None, remarks_fields)))

remarks.text = _remarks
detail.append(remarks)

root = ET.Element("event")
root.set("version", "2.0")
root.set("type", cot_type)
root.set("uid", cot_uid)
root.set("how", "m-g")
root.set("time", pytak.cot_time())
root.set("start", pytak.cot_time())
root.set("stale", pytak.cot_time(cot_stale))

root.append(point)
root.append(detail)

return root

def incident_to_cot_xml(
incident: dict,
Expand Down Expand Up @@ -172,6 +262,15 @@ def incident_to_cot(
b"\n".join([pytak.DEFAULT_XML_DECLARATION, ET.tostring(cot)]) if cot else None
)

def aed_to_cot(
call: dict, config: Union[dict, None] = None, agency: Union[dict, None] = None
) -> Union[bytes, None]:
"""Wrapper that returns COT as an XML string."""
cot: Union[ET.Element, None] = aed_to_cot_xml(call, config, agency)
return (
b"\n".join([pytak.DEFAULT_XML_DECLARATION, ET.tostring(cot)]) if cot else None
)


# [{'AddressTruncated': '1',
# 'AgencyID': 'EMS1384',
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def publish():
description="PulsePoint to Cursor-On-Target Gateway for ATAK and other TAK systems.",
author="Greg Albrecht",
author_email="[email protected]",
package_data={"": ["LICENSE"]},
package_data={"": ["LICENSE", "data/*.json"]},
license="Apache License, Version 2.0",
long_description=open("README.rst").read(),
long_description_content_type="text/x-rst",
Expand All @@ -64,6 +64,5 @@ def publish():
"Programming Language :: Python",
"License :: OSI Approved :: Apache Software License",
],
keywords=["CAD", "Cursor on Target", "ATAK", "TAK", "COT"],
package_data={'': ['data/*.json']},
keywords=["CAD", "Cursor on Target", "ATAK", "TAK", "COT"]
)