add code
This commit is contained in:
parent
59fca41ffa
commit
d1110dece8
|
@ -1,3 +1,6 @@
|
||||||
|
.idea
|
||||||
|
config.yml
|
||||||
|
|
||||||
# ---> Python
|
# ---> Python
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
@ -15,7 +18,6 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|
18
README.md
18
README.md
|
@ -1,3 +1,19 @@
|
||||||
# ha-location-exporter
|
# ha-location-exporter
|
||||||
|
|
||||||
Export your device location and save it in a database.
|
*Export your device location and save it in a database.*
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get install mariadb-client libmariadb-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cp config.sample.yml config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, edit `config.yml`.
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
ha_url: https://example.com
|
||||||
|
access_token: abc123
|
||||||
|
entities:
|
||||||
|
- device_tracker.your_phone
|
||||||
|
|
||||||
|
interval: 1800
|
||||||
|
|
||||||
|
database:
|
||||||
|
host: 127.0.0.1
|
||||||
|
database: ha_locations
|
||||||
|
username: ha_locations
|
||||||
|
password: pw123
|
||||||
|
|
||||||
|
dawarich:
|
||||||
|
host: http://127.0.0.1:3000
|
||||||
|
api_key: abc123
|
||||||
|
|
||||||
|
# Only one entity is supported on Dawarich.
|
||||||
|
entity_id: device_tracker.your_phone
|
|
@ -0,0 +1,13 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Home Assistant Location Export
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=homeassistant
|
||||||
|
ExecStart=/srv/ha-location-exporter/ha-location-exporter/venv/bin/python /srv/ha-location-exporter/ha-location-exporter/main.py
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
|
@ -0,0 +1,46 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
_logger = logging.getLogger('MAIN').getChild('DAWARICH')
|
||||||
|
|
||||||
|
|
||||||
|
def send_to_dawarich(entity_id, timestamp, latitude, longitude, gps_accuracy, vertical_accuracy, altitude, battery, speed_kph, config_data):
|
||||||
|
url = f'{config_data["dawarich"]["host"]}/api/v1/overland/batches?api_key={config_data["dawarich"]["api_key"]}'
|
||||||
|
data = {
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [
|
||||||
|
longitude,
|
||||||
|
latitude
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"api_key": config_data['dawarich']['api_key'],
|
||||||
|
"timestamp": timestamp.isoformat(),
|
||||||
|
"altitude": altitude,
|
||||||
|
"speed": speed_kph,
|
||||||
|
"horizontal_accuracy": gps_accuracy,
|
||||||
|
"vertical_accuracy": vertical_accuracy,
|
||||||
|
"motion": [],
|
||||||
|
"pauses": False,
|
||||||
|
"activity": "",
|
||||||
|
"desired_accuracy": 0,
|
||||||
|
"deferred": 0,
|
||||||
|
"significant_change": "unknown",
|
||||||
|
"locations_in_payload": 1,
|
||||||
|
"device_id": entity_id,
|
||||||
|
"wifi": "unknown",
|
||||||
|
"battery_state": "unknown",
|
||||||
|
"battery_level": battery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
if response.status_code != 201:
|
||||||
|
_logger.error(f'Failed to send data to dawarich for entity "{entity_id}": {response.status_code} -- {response.text}')
|
|
@ -0,0 +1,16 @@
|
||||||
|
from sqlalchemy import Column, Integer, Text, DateTime, Numeric, JSON
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class HALocations(Base):
|
||||||
|
__tablename__ = 'ha_locations'
|
||||||
|
|
||||||
|
device = Column(Text, nullable=False)
|
||||||
|
lat = Column(Numeric(precision=20, scale=14), nullable=False)
|
||||||
|
lon = Column(Numeric(precision=20, scale=14), nullable=False)
|
||||||
|
state = Column(Text, nullable=False)
|
||||||
|
attributes = Column(JSON, nullable=False, default={})
|
||||||
|
timestamp = Column(DateTime(timezone=True), nullable=False)
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
@ -0,0 +1,3 @@
|
||||||
|
def mysql_trim_float(f):
|
||||||
|
s = str(f)
|
||||||
|
return float(s.split('.')[0] + '.' + s.split('.')[-1][:14])
|
|
@ -0,0 +1,146 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import yaml
|
||||||
|
from dateparser import parse
|
||||||
|
from sqlalchemy import create_engine, func
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from lib.dawarich import send_to_dawarich
|
||||||
|
from lib.models import HALocations, Base
|
||||||
|
from lib.strings import mysql_trim_float
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
_logger = logging.getLogger('MAIN')
|
||||||
|
|
||||||
|
CONFIG_DATA = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_battery_level(entity_id, timestamp):
|
||||||
|
battery_entity_id = f"sensor.{entity_id.split('.')[1]}_battery_level"
|
||||||
|
start_time = timestamp - timedelta(minutes=1)
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
f"{CONFIG_DATA['ha_url']}/api/history/period/{start_time.isoformat()}?filter_entity_id={battery_entity_id}",
|
||||||
|
headers={"Authorization": f"Bearer {CONFIG_DATA['access_token']}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
battery_history = response.json()[0]
|
||||||
|
if battery_history:
|
||||||
|
for entry in reversed(battery_history):
|
||||||
|
entry_timestamp = parse(entry["last_changed"])
|
||||||
|
if entry_timestamp <= timestamp:
|
||||||
|
state = entry.get("state")
|
||||||
|
return float(state) if state is not None else None
|
||||||
|
_logger.warning(f'No battery level data found for entity "{battery_entity_id}" at or before timestamp {timestamp}')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
_logger.warning(f'No battery level data found for entity "{battery_entity_id}" at or before timestamp {timestamp}')
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
_logger.error(f'Failed to fetch battery level for entity "{battery_entity_id}" at or before timestamp {timestamp}: {response.status_code} -- {response.text}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global CONFIG_DATA
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
config_path = Path(os.path.dirname(os.path.realpath(__file__)), 'config.yml')
|
||||||
|
if not config_path.exists():
|
||||||
|
_logger.critical(f'Config file not found: {config_path}')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(config_path) as stream:
|
||||||
|
try:
|
||||||
|
CONFIG_DATA = yaml.safe_load(stream)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
_logger.critical(f'Failed to load config file: {e}')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
_logger.info('Initalizing database...')
|
||||||
|
engine = create_engine(f'mysql://{CONFIG_DATA["database"]["username"]}:{CONFIG_DATA["database"]["password"]}@{CONFIG_DATA["database"]["host"]}{":" + str(CONFIG_DATA["database"]["port"]) if CONFIG_DATA["database"].get("port") else ""}/{CONFIG_DATA["database"]["database"]}')
|
||||||
|
Session = sessionmaker(bind=engine)
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
end_time = datetime.now()
|
||||||
|
start_time = end_time - timedelta(hours=24)
|
||||||
|
|
||||||
|
for entity_id in CONFIG_DATA['entities']:
|
||||||
|
_logger.info(f'Fetching entity "{entity_id}"')
|
||||||
|
response = requests.get(
|
||||||
|
f"{CONFIG_DATA['ha_url']}/api/history/period/{start_time.isoformat()}?filter_entity_id={entity_id}",
|
||||||
|
headers={"Authorization": f"Bearer {CONFIG_DATA['access_token']}"},
|
||||||
|
)
|
||||||
|
|
||||||
|
new_rows = 0
|
||||||
|
skipped_rows = 0
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
location_history = response.json()[0]
|
||||||
|
session = Session()
|
||||||
|
|
||||||
|
for entry in location_history:
|
||||||
|
timestamp = parse(entry["last_changed"])
|
||||||
|
state = entry["state"]
|
||||||
|
attributes = entry["attributes"]
|
||||||
|
latitude = mysql_trim_float(attributes.get("latitude"))
|
||||||
|
longitude = mysql_trim_float(attributes.get("longitude"))
|
||||||
|
|
||||||
|
existing_entry = session.query(HALocations).filter(
|
||||||
|
func.round(HALocations.lat, 6) == round(latitude, 6),
|
||||||
|
func.round(HALocations.lon, 6) == round(longitude, 6),
|
||||||
|
func.date(HALocations.timestamp) == datetime.strptime(str(timestamp).strip('+00:00'), '%Y-%m-%d %H:%M:%S.%f').date(),
|
||||||
|
func.hour(HALocations.timestamp) == datetime.strptime(str(timestamp).strip('+00:00'), '%Y-%m-%d %H:%M:%S.%f').hour,
|
||||||
|
func.minute(HALocations.timestamp) == datetime.strptime(str(timestamp).strip('+00:00'), '%Y-%m-%d %H:%M:%S.%f').minute,
|
||||||
|
HALocations.device == entity_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_entry is None:
|
||||||
|
batt_level = get_battery_level(entity_id, timestamp)
|
||||||
|
if batt_level is not None:
|
||||||
|
attributes['battery'] = batt_level
|
||||||
|
else:
|
||||||
|
attributes['battery'] = -1
|
||||||
|
|
||||||
|
new_entry = HALocations(
|
||||||
|
device=entity_id,
|
||||||
|
lat=latitude,
|
||||||
|
lon=longitude,
|
||||||
|
state=state,
|
||||||
|
attributes=attributes,
|
||||||
|
timestamp=timestamp
|
||||||
|
)
|
||||||
|
session.add(new_entry)
|
||||||
|
|
||||||
|
if CONFIG_DATA.get('dawarich') and entity_id == CONFIG_DATA['dawarich']['entity_id']:
|
||||||
|
send_to_dawarich(
|
||||||
|
entity_id,
|
||||||
|
timestamp,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
attributes['gps_accuracy'], attributes['vertical_accuracy'], attributes['altitude'], attributes['battery'], attributes['speed'], CONFIG_DATA)
|
||||||
|
|
||||||
|
new_rows += 1
|
||||||
|
else:
|
||||||
|
skipped_rows += 1
|
||||||
|
|
||||||
|
_logger.info(f'Committing changes to database for entity "{entity_id}". New: {new_rows}. Skipped: {skipped_rows}')
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
else:
|
||||||
|
_logger.error(f'Failed to fetch the location history: {response.status_code} -- {response.text}')
|
||||||
|
|
||||||
|
_logger.info('History fetch complete')
|
||||||
|
time.sleep(CONFIG_DATA['interval'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,5 @@
|
||||||
|
requests==2.32.3
|
||||||
|
sqlalchemy==2.0.33
|
||||||
|
pyyaml==6.0.2
|
||||||
|
mysqlclient==2.2.4
|
||||||
|
dateparser==1.2.0
|
Loading…
Reference in New Issue