add code
This commit is contained in:
parent
59fca41ffa
commit
d1110dece8
|
@ -1,3 +1,6 @@
|
|||
.idea
|
||||
config.yml
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
@ -15,7 +18,6 @@ dist/
|
|||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
|
18
README.md
18
README.md
|
@ -1,3 +1,19 @@
|
|||
# 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