diff --git a/custom_components/history_services/common.py b/custom_components/history_services/common.py index a79dff4..659c8ce 100644 --- a/custom_components/history_services/common.py +++ b/custom_components/history_services/common.py @@ -24,7 +24,8 @@ def get_significant_states(hass: HomeAssistant, call: ServiceCall): start_time = now - ONE_DAY end_time = now - entity_ids = list([call.data["entity_id"]]) + entity_id = call.data["entity_id"] + entity_ids = list([entity_id]) if "start" in call.data: start_time = dt_util.as_utc(call.data["start"]) @@ -34,10 +35,10 @@ def get_significant_states(hass: HomeAssistant, call: ServiceCall): start_time_local = dt_util.as_local(start_time).isoformat() end_time_local = dt_util.as_local(end_time).isoformat() - timespan_local = { "start": start_time_local, "end": end_time_local } + timespan = { "start": start_time_local, "end": end_time_local } if start_time > now: - return { "timespan": timespan_local, "result": "error", "message": "Invalid date" } + return { "timespan": timespan, "result": "error", "message": "Invalid date" } include_start_time_state = True significant_changes_only = True @@ -57,6 +58,9 @@ def get_significant_states(hass: HomeAssistant, call: ServiceCall): ) if not response: - return { "timespan": timespan_local, "result": "", "message": "Request returned empty response" } + return { "timespan": timespan, "result": "", "message": "Request returned empty response" } - return { "timespan": timespan_local, "result": response[call.data["entity_id"]] } + result = response[entity_id] + result.sort(key = lambda i: i.last_updated) + + return { "timespan": timespan, "result": result } diff --git a/custom_components/history_services/const.py b/custom_components/history_services/const.py index d612e7f..6431e52 100644 --- a/custom_components/history_services/const.py +++ b/custom_components/history_services/const.py @@ -6,8 +6,6 @@ DOMAIN = "history_services" -CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) - EXPORT_SERVICE_NAME = "export" EXPORT_DEVICE_TRACKER_SERVICE_NAME = "export_device_tracker" diff --git a/custom_components/history_services/manifest.json b/custom_components/history_services/manifest.json index 90d0a26..b676cd3 100644 --- a/custom_components/history_services/manifest.json +++ b/custom_components/history_services/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/davidrapan/ha-history/issues", "requirements": ["simplekml"], - "version": "0.1.0" + "version": "0.1.1" } diff --git a/custom_components/history_services/services.yaml b/custom_components/history_services/services.yaml index f751c30..ed78964 100644 --- a/custom_components/history_services/services.yaml +++ b/custom_components/history_services/services.yaml @@ -65,7 +65,7 @@ export_device_tracker: name: Attributes description: Additional attributes to include required: false - example: timestamp course speed + example: timestamp distance length course speed selector: text: filepath: diff --git a/custom_components/history_services/services/device_tracker.py b/custom_components/history_services/services/device_tracker.py index e95a51d..505f8d5 100644 --- a/custom_components/history_services/services/device_tracker.py +++ b/custom_components/history_services/services/device_tracker.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import math import logging import asyncio @@ -54,7 +55,15 @@ def segment_condition(attributes1, attributes2): if "speed" in attributes1 and int(attributes1["speed"]) > 0: return True elif attributes2: - return haversine2(attributes1, attributes2) > 0.019 + return h > 0.019 + return False + +def segment_condition2(attributes): + if is_gps(attributes): + if "speed" in attributes and int(attributes["speed"]) > 0: + return True + elif "distance" in attributes and attributes["distance"] > 0.009: + return True return False def are_coords_within(points, radius): @@ -90,40 +99,58 @@ async def async_register_service(hass: HomeAssistant): @callback async def export_service(call: ServiceCall) -> ServiceResponse: response = get_significant_states(hass, call) - result = response["result"] + response_result = response["result"] - if not result: + if not response_result: return response - if not is_gps(result[0].attributes): + if not is_gps(response_result[0].attributes): return { "timespan": response["timespan"], "result": "error", "message": "Entity is not a gps tracker" } + result = [] + # Why is it necessary to do deepcopy when enriching state? + # - It messes up the values of those added attributes (duplicates, etc.) + # - Maybe because of the caching nature of LazyState from recorder? + for i, p in enumerate(response_result): + point = copy.deepcopy(p) + point.attributes["timestamp"] = dt_util.as_local(point.last_updated).isoformat() + if i > 0: + point.attributes["distance"] = haversine2(result[i - 1].attributes, point.attributes) + point.attributes["length"] = timediff(result[i - 1].last_updated, point.last_updated) + else: + point.attributes["distance"] = 0 + point.attributes["length"] = 0 + result.append(point) + + min_radius = call.data["min_radius"] / 1000 + max_gap = td(seconds = call.data["max_gap"]) + segments = [] current_segment = [] result_last = len(result) - 1 - for i, point in enumerate(result): - point.attributes["timestamp"] = dt_util.as_local(point.last_changed).isoformat() - if segment_condition(point.attributes, result[i + 1].attributes if i < result_last else []): - if current_segment and i > 0: + prevp = None + for i, p in enumerate(result): + if segment_condition2(p.attributes): + if not current_segment and i > 0: current_segment.append(result[i - 1]) - current_segment.append(point) + current_segment.append(p) else: if current_segment: - if len(current_segment) > 1 and i < result_last: - current_segment.append(result[i + 1]) - segments.append(current_segment) + #if i < result_last and haversine2(point.attributes, result[i + 1].attributes) > 0: + # result[i + 1].attributes["distance"] = haversine2(current_segment[-1].attributes, result[i + 1].attributes) + # current_segment.append(result[i + 1]) + #current_segment[0].attributes["distance"] = 0 + if len(current_segment) > 1 and not are_coords_within([(c.attributes["latitude"], c.attributes["longitude"]) for c in current_segment], min_radius): + segments.append(current_segment) current_segment = [] - min_radius = call.data["min_radius"] / 1000 - max_gap = td(seconds = call.data["max_gap"]) - connected_segments = [] current_segment = [] for i, segment in enumerate(segments): - if are_coords_within([(c.attributes["latitude"], c.attributes["longitude"]) for c in segment], min_radius): - continue - - if not current_segment or timediff(segment[0].last_changed, current_segment[-1].last_changed) <= max_gap: + if not current_segment or timediff(current_segment[-1].last_updated, segment[0].last_updated) <= max_gap: + #if not current_segment or point.attributes["length"] <= max_gap: + segment[0].attributes["distance"] = haversine2(current_segment[-1].attributes, segment[0].attributes) if i > 0 else 0 + segment[0].attributes["length"] = timediff(current_segment[-1].last_updated, segment[0].last_updated) if i > 0 else 0 current_segment.extend(segment) else: connected_segments.append(current_segment) @@ -142,15 +169,20 @@ async def export_service(call: ServiceCall) -> ServiceResponse: if attributes: schema = kml.newschema() - attributes_list = [i for i in attributes.split() if i in connected_segments[0][0].attributes] + attributes_list = [i for i in attributes.split() if i in result[0].attributes] for item in attributes_list: - type = simplekml.Types.int if isinstance(connected_segments[0][0].attributes[item], int) else simplekml.Types.string + type = simplekml.Types.int if isinstance(result[0].attributes[item], int) else simplekml.Types.string schema.newgxsimplearrayfield(name = item, type = type, displayname = item.capitalize()) for s in connected_segments: - linestring = kml.newlinestring(name = (str(dt_util.as_local(s[0].last_changed)) + " - " + str(dt_util.as_local(s[-1].last_changed)))) - linestring.timespan.begin = str(s[0].last_changed) - linestring.timespan.end = str(s[-1].last_changed) + l = 0 + for p in s: + l += p.attributes["distance"] + l = round(l, 3) + t = s[-1].last_updated - s[0].last_updated + linestring = kml.newlinestring(name = (str(dt_util.as_local(s[0].last_updated)) + " - " + str(dt_util.as_local(s[-1].last_updated)) + ", duration: " + str(t) + ", length: " + str(l) + " km")) + linestring.timespan.begin = str(s[0].last_updated) + linestring.timespan.end = str(s[-1].last_updated) linestring.coords = [(p.attributes["longitude"], p.attributes["latitude"], p.attributes["altitude"]) for p in s] if attributes: linestring.extendeddata.schemadata.schemaurl = schema.id diff --git a/readme.md b/readme.md index da21782..370f216 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ Saves output into file with default location: 'www/history/device_tracker.kml' - Follow the link [here](https://hacs.xyz/docs/faq/custom_repositories/) - Add custom repository: https://github.com/davidrapan/ha-history - Select type of the category: integration -- Find newly added History Services and click on the INSTALL button +- Find newly added History Services, open it and then click on the DOWNLOAD button ### Manually - Copy the contents of 'custom_components/history_services' directory into the Home Assistant with exactly the same hirearchy withing the '/config' directory