CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

jbaruch/nanoclaw-travel

Travel assistant for NanoClaw: byAir flight notifications (delay, gate, connection risk, inbound aircraft delay, time-to-leave, arrival logistics), traffic-aware drive planning for in-person meetings (auto drive blocks + leave-by traffic rechecks), travel-booking gap checks, and nightly TripIt sync. Per-chat overlay plugin.

Quality

90%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

ev_charging.pyskills/drive-engine/

"""Pure, conservative EV time planning over the complete chronological drive set.

Inputs are measurements from the supported Tesla estimate provider, route distance,
and explicitly grounded charging observations. No I/O, vehicle control, charger
selection or persistence lives here. A stop is a minimum energy stop assuming a
full usable recharge; configured minutes are an allowance, never a station promise.
The measured weighted driving rate is NOT a measured highway-only rate. Explicit
cold/highway allowances remain separate from that measurement.
"""

from __future__ import annotations

import math
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass, replace
from datetime import datetime, timedelta

from reconcile import DEPART_AFTER_KINDS, DesiredBlock


@dataclass(frozen=True)
class EnergyGap:
    """An existing unplanned drive; unknown start/vehicle fences all possible uses."""

    start: datetime | None
    vehicle: str | None = None


MAX_STOPS = 24
MAX_LEGS = 2000
FORECAST_HORIZON = timedelta(days=14)
MEASUREMENT_AGE = timedelta(days=90)
NAVIGATION_AGE = timedelta(minutes=5)
NAVIGATION_HORIZON = timedelta(minutes=15)


def finite(value: object, low: float, high: float) -> bool:
    return (
        isinstance(value, (int, float))
        and not isinstance(value, bool)
        and math.isfinite(value)
        and low <= value <= high
    )


def place(value: str) -> str:
    return " ".join(value.split()).casefold()


@dataclass(frozen=True)
class PlanningOptions:
    stop_minutes: int = 30
    reserve_percent: int = 10
    highway_fraction: float = 0.1
    cold_fraction: float = 0.2

    def __post_init__(self):
        if (
            type(self.stop_minutes) is not int
            or not 1 <= self.stop_minutes <= 240
            or type(self.reserve_percent) is not int
            or not 0 <= self.reserve_percent <= 50
            or not finite(self.highway_fraction, 0, 2)
            or not finite(self.cold_fraction, 0, 2)
        ):
            raise ValueError("Use bounded charging-time, reserve and energy allowances.")


@dataclass(frozen=True)
class EnergySample:
    vehicle: str
    capacity_kwh: float
    measured_kwh_per_km: float
    measured_at: datetime
    departure_percent: float

    def valid(self, vehicle: str, now: datetime) -> bool:
        return (
            self.vehicle == vehicle
            and finite(self.capacity_kwh, 0.001, 1000)
            and finite(self.measured_kwh_per_km, 0.000001, 10)
            and finite(self.departure_percent, 0, 100)
            and self.measured_at.tzinfo is not None
            and now - MEASUREMENT_AGE <= self.measured_at <= now
        )


@dataclass(frozen=True)
class DestinationCharge:
    """Positive owner/provider evidence, bound to one vehicle, place and time.

    Mere lodging, multi-day duration, a charger POI or a guessed overnight target
    is not evidence. This record describes a known delivered/forecast energy
    amount and its availability instant; the adapter must retain its provenance.
    """

    vehicle: str
    destination: str
    available_at: datetime
    energy_kwh: float


@dataclass(frozen=True)
class Navigation:
    """Cached owner-only active route, never a request to wake or navigate a car."""

    vehicle: str
    destination: str
    observed_at: datetime
    minutes_to_arrival: float
    arrival_percent: float

    def matches(self, block: DesiredBlock, vehicle: str, now: datetime) -> bool:
        return (
            self.vehicle == vehicle
            and place(self.destination) == place(block.destination)
            and bool(place(self.destination))
            and self.observed_at.tzinfo is not None
            and now - NAVIGATION_AGE <= self.observed_at <= now
            and block.end > now
            and block.start <= now + NAVIGATION_HORIZON
            and finite(self.minutes_to_arrival, 0.01, 1440)
            and finite(self.arrival_percent, 0, 100)
        )


Estimate = Callable[[str, float, datetime], EnergySample | None]


def _shift(block: DesiredBlock, extra: timedelta, note: str) -> DesiredBlock:
    # Return/arrival drives start after a preceding event; all other drives
    # arrive by their anchor. This preserves their existing scheduling contract.
    after = block.kind in DEPART_AFTER_KINDS
    return replace(
        block,
        start=block.start if after else block.start - extra,
        end=block.end + extra if after else block.end,
        charging_note=note,
    )


def adjust_charging(
    blocks: Sequence[DesiredBlock],
    *,
    vehicles: Mapping[tuple[str, str], str],
    distance_km: Callable[[DesiredBlock], float | None],
    estimate: Estimate,
    now: datetime,
    options: PlanningOptions | None = None,
    destination_charges: Sequence[DestinationCharge] = (),
    navigation: Sequence[Navigation] = (),
    protected_windows: Sequence[tuple[datetime, datetime]] = (),
    energy_gaps: Sequence[EnergyGap] = (),
    retained_starts: Mapping[tuple[str, str], datetime] | None = None,
    unavailable: Callable[[frozenset[tuple[str, str]]], None] | None = None,
) -> list[DesiredBlock]:
    """Return adjusted blocks; ungrounded dependent legs retain ORIGINAL objects.

    Every later leg consumes the prior leg's remaining energy. Independently
    forecasted return percentages never replenish it. Only exact positive
    destination-charge evidence can add energy between connected legs. Missing
    distance, measurement, vehicle or route continuity keeps the whole vehicle
    dependent chain unchanged instead of producing an optimistic return battery.
    Out-of-horizon legs remain unchanged without suppressing nearer drives.
    Disconnected endpoints do not replenish the same vehicle's remaining energy.

    A bounded fixed-point pass forecasts the first leg at its actual earlier
    leave-by, rather than counting charging that would occur after it must leave.
    No effect retry is involved: estimate is a read-only dependency.
    """
    original = list(blocks)
    options = options or PlanningOptions()
    retained_starts = retained_starts or {}
    if now.tzinfo is None or len(original) > MAX_LEGS:
        if unavailable is not None:
            unavailable(frozenset((block.identity, block.kind) for block in original))
        return original
    grouped: dict[str, list[tuple[int, DesiredBlock]]] = {}
    eligible_keys = {
        (vehicles.get((block.identity, block.kind)), block.identity, block.kind)
        for block in original
        if isinstance(vehicles.get((block.identity, block.kind)), str)
        and vehicles.get((block.identity, block.kind))
        and block.end > now
        and block.start <= now + FORECAST_HORIZON
    }
    return_predecessors = {
        "attendance_return": "attendance_outbound",
        "meeting_return": "meeting_outbound",
        "lodging_return": "lodging_outbound",
        "airport_arrival": "airport_departure",
    }
    unknown_starts = sorted(
        block.start
        for block in original
        if (block.identity, block.kind) not in vehicles
        and block.end > now
        and block.start <= now + FORECAST_HORIZON
    )
    for index, block in enumerate(original):
        vehicle = vehicles.get((block.identity, block.kind))
        if (
            isinstance(vehicle, str)
            and vehicle.strip()
            and block.end > now
            and block.start <= now + FORECAST_HORIZON
        ):
            grouped.setdefault(vehicle, []).append((index, block))
    for vehicle, chain in grouped.items():
        starts = sorted(
            [
                *unknown_starts,
                *(
                    gap.start or now
                    for gap in energy_gaps
                    if gap.vehicle is None or gap.vehicle == vehicle
                ),
            ]
        )
        chain.sort(key=lambda item: (item[1].start, item[1].end, item[0]))
        segments: list[list[tuple[int, DesiredBlock]]] = []
        segment: list[tuple[int, DesiredBlock]] = []
        unknown_index = 0
        grounded = True
        last_unknown: datetime | None = None
        ungrounded = set()
        discovered_starts = []
        for item in chain:
            _, block = item
            crossed_unknown = False
            overlapped_unknown = False
            while unknown_index < len(starts) and starts[unknown_index] < block.end:
                grounded = False
                crossed_unknown = True
                overlapped_unknown |= starts[unknown_index] > block.start
                last_unknown = starts[unknown_index]
                unknown_index += 1
            predecessor = return_predecessors.get(block.kind)
            if predecessor and (vehicle, block.identity, predecessor) not in eligible_keys:
                grounded = False
                crossed_unknown = True
            if crossed_unknown and segment:
                segments.append(segment)
                segment = []
            if not overlapped_unknown and any(
                route.matches(block, vehicle, now)
                and (last_unknown is None or route.observed_at >= last_unknown)
                and route.arrival_percent >= options.reserve_percent
                for route in navigation
            ):
                # Exact fresh active-route arrival energy independently grounds
                # this leg and later uses, even after an unknown intervening drive.
                grounded = True
            if grounded:
                segment.append(item)
            else:
                key = (block.identity, block.kind)
                effective_start = min(block.start, retained_starts.get(key, block.start))
                discovered_starts.append(effective_start)
                last_unknown = (
                    max(last_unknown, effective_start)
                    if last_unknown is not None
                    else effective_start
                )
                ungrounded.add(key)
        # Later missing predecessors can expose a retained start before their
        # route-only start. Earlier segments must see those newly found fences.
        starts = sorted([*starts, *discovered_starts])
        if segment:
            segments.append(segment)
        if ungrounded and unavailable is not None:
            unavailable(frozenset(ungrounded))
        for segment in segments:
            planned = _vehicle_chain(
                segment,
                vehicle,
                distance_km,
                estimate,
                now,
                options,
                destination_charges,
                navigation,
                protected_windows,
            )
            if planned is not None:
                # An earlier departure produced by charging/navigation also
                # cannot cross a possible drive that the original span avoided.
                crossed = next(
                    (
                        index
                        for index, block in enumerate(planned)
                        if any(block.start < start < block.end for start in starts)
                    ),
                    len(planned),
                )
                if crossed < len(planned) and unavailable is not None:
                    unavailable(
                        frozenset((block.identity, block.kind) for _, block in segment[crossed:])
                    )
                for (index, _), block in zip(segment[:crossed], planned[:crossed], strict=True):
                    original[index] = block
            elif unavailable is not None:
                unavailable(frozenset((block.identity, block.kind) for _, block in segment))
    return original


def _vehicle_chain(
    chain, vehicle, distance_km, estimate, now, options, charges, navigation, windows
):
    distances: list[float] = []
    prior: DesiredBlock | None = None
    for _, block in chain:
        distance = distance_km(block)
        if (
            not finite(distance, 0.001, 100000)
            or block.start.tzinfo is None
            or block.end.tzinfo is None
            or block.end <= block.start
            or block.start > now + FORECAST_HORIZON
            or (prior is not None and prior.end > block.start)
        ):
            return None
        distances.append(float(distance))
        prior = block
    first = chain[0][1]
    departure = max(now, first.start)
    for _ in range(4):
        sample = estimate(vehicle, distances[0], departure)
        if sample is None or not sample.valid(vehicle, now):
            return None
        capacity = sample.capacity_kwh
        remaining = capacity * sample.departure_percent / 100
        reserve = capacity * options.reserve_percent / 100
        per_stop = capacity - reserve
        results: list[DesiredBlock] = []
        used_charges: set[int] = set()
        for position, ((_, block), distance) in enumerate(zip(chain, distances, strict=True)):
            charging_ready: list[datetime] = []
            if position:
                previous = results[-1]
                for index, charge in enumerate(charges):
                    if (
                        index not in used_charges
                        and charge.vehicle == vehicle
                        and place(previous.destination) == place(block.origin)
                        and place(charge.destination) == place(block.origin)
                        and charge.available_at.tzinfo is not None
                        and previous.end <= charge.available_at <= block.start
                        and finite(charge.energy_kwh, 0.001, 1000)
                    ):
                        remaining = min(capacity, remaining + charge.energy_kwh)
                        used_charges.add(index)
                        charging_ready.append(charge.available_at)
            energy = (
                distance
                * sample.measured_kwh_per_km
                * (1 + options.cold_fraction)
                * (1 + options.highway_fraction)
            )
            stops = max(0, math.ceil(max(0, energy + reserve - remaining) / per_stop - 1e-12))
            if stops > MAX_STOPS:
                return None
            remaining += stops * per_stop - energy
            extra = timedelta(minutes=stops * options.stop_minutes)
            result = block
            if stops:
                noun = "stop" if stops == 1 else "stops"
                note = f"{stops} charging {noun}, ~{stops * options.stop_minutes} min"
                result = _shift(block, extra, note)
            routes = [route for route in navigation if route.matches(block, vehicle, now)]
            native_route_used = False
            if len(routes) > 1:
                return None
            if routes and routes[0].arrival_percent >= options.reserve_percent:
                native_route_used = True
                nav = routes[0]
                duration = timedelta(minutes=nav.minutes_to_arrival)
                result = _shift(
                    block,
                    duration - (block.end - block.start),
                    "Car navigation ETA",
                )
                if block.start < now:
                    # Native remaining ETA is measured from now. Preserve the
                    # actual block's past start rather than backdating a new
                    # departure or treating remaining minutes as total time.
                    result = replace(
                        result,
                        start=block.start,
                        end=now + duration,
                    )
                remaining = capacity * nav.arrival_percent / 100
            if (
                (result.start < now and not (native_route_used and result.start == block.start))
                or (block.earliest_start is not None and result.start < block.earliest_start)
                or any(result.start < ready for ready in charging_ready)
                or (results and results[-1].end > result.start)
            ):
                return None
            if result != block and any(
                (result.start < block.start and result.start < end and block.start > start)
                or (result.end > block.end and block.end < end and result.end > start)
                for start, end in windows
            ):
                return None
            results.append(result)
        actual_departure = max(now, results[0].start)
        if actual_departure == departure:
            return results
        departure = actual_departure
    return None

skills

README.md

tile.json