Skill: hiimivantang/byd-remote-control (version 1.0.0)

--- name: byd-remote-control description: Control and check a BYD vehicle using portable Python helper scripts built on pyBYD. Use when the user wants to check battery state, lock the car, flash lights, start A/C, stop A/C, or build/operate reusable BYD remote-control automation from local scripts and environment variables. --- # BYD Remote Control Use the bundled Python scripts in `scripts/` for repeatable BYD actions instead of rewriting the same pyBYD code each time. ## Available scripts - `scripts/battery_check.py` , fetch battery %, range, charging state, and timestamps as JSON - `scripts/battery_monitor.py` , fetch battery state and emit an alert payload when battery is below `BATTERY_THRESHOLD` - `scripts/lock_car.py` , lock the vehicle - `scripts/flash_lights.py` , flash lights / horn - `scripts/start_ac.py` , start climate control - `scripts/stop_ac.py` , stop climate control - `scripts/byd_common.py` , shared helper module used by the scripts ## How to use 1. Read `references/setup.md` if credentials or dependencies are not already configured. 2. Run the relevant script from the skill directory so `.env` is discovered correctly. 3. Prefer the existing script over ad hoc inline Python when the task matches one of the bundled actions. ## Notes - The scripts load `.env` from the same directory as the scripts. - `BYD_PIN` is automatically bridged to `BYD_CONTROL_PIN`. - Vehicle selection can be controlled with `BYD_VIN` or `BYD_VEHICLE_ALIAS`. If neither is set, the scripts use the first vehicle returned by the account. - `battery_monitor.py` prints alert JSON when below threshold. Delivery should be handled by the caller or automation layer.

path: references/setup.md
content: # Setup

Put a `.env` file in `skills/byd-remote-control/scripts/` with:

- `BYD_USERNAME`
- `BYD_PASSWORD`
- `BYD_BASE_URL` (optional if the default works)
- `BYD_PIN` or `BYD_CONTROL_PIN`

Optional vehicle selection:

- `BYD_VIN` to target a specific vehicle by VIN
- `BYD_VEHICLE_ALIAS` to target a specific vehicle by alias

Install Python dependency:

- `pybyd`

Optional:

- `BATTERY_THRESHOLD` for `battery_monitor.py`

Example usage:

- `python3 scripts/battery_check.py`
- `python3 scripts/lock_car.py`
- `python3 scripts/start_ac.py`
- `python3 scripts/stop_ac.py`

If no vehicle selection env var is set, the scripts use the first vehicle returned by the account.


path: scripts/battery_check.py
content: #!/usr/bin/env python3
"""Fetch BYD battery snapshot and print JSON."""
from __future__ import annotations

import asyncio
import datetime as dt
import json
from typing import Any

from pybyd import BydClient

from byd_common import create_config, get_target_vehicle


def _as_timestamp(value: Any) -> float | None:
    if value is None:
        return None
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, dt.datetime):
        return value.timestamp()
    return None


def _ts_iso(ts: Any) -> str | None:
    parsed = _as_timestamp(ts)
    if parsed is None:
        return None
    return dt.datetime.fromtimestamp(parsed, tz=dt.timezone.utc).isoformat()


async def main() -> None:
    config = create_config()
    async with BydClient(config) as client:
        vehicle = await get_target_vehicle(client)
        vin = vehicle.vin
        realtime = await client.get_vehicle_realtime(vin)
        if realtime.elec_percent is None:
            raise RuntimeError("BYD API returned no battery percentage (elec_percent is None)")
        ts_num = _as_timestamp(realtime.timestamp)
        data = {
            "vin": vin,
            "soc_percent": realtime.elec_percent,
            "endurance_km": realtime.endurance_mileage or realtime.ev_endurance,
            "charging_state": realtime.effective_charging_state.name,
            "timestamp": ts_num,
            "timestamp_iso": _ts_iso(realtime.timestamp),
        }
        print(json.dumps(data, ensure_ascii=False))


if __name__ == "__main__":
    asyncio.run(main())


path: scripts/battery_monitor.py
content: #!/usr/bin/env python3
"""
BYD Battery Monitor - Checks vehicle battery level and reports whether it is below threshold.
"""

import asyncio
import json
import os
from datetime import datetime

from pybyd import BydClient

from byd_common import create_config, get_target_vehicle

BATTERY_THRESHOLD = int(os.environ.get("BATTERY_THRESHOLD", "50"))  # Alert if battery falls below this percentage


async def get_battery_snapshot() -> dict | None:
    """Query BYD API for current battery snapshot via pyBYD."""
    try:
        config = create_config()
        async with BydClient(config) as client:
            vehicle = await get_target_vehicle(client)
            vin = vehicle.vin
            realtime = await client.get_vehicle_realtime(vin)
            if realtime.elec_percent is None:
                return None
            return {
                "battery_level": int(realtime.elec_percent),
                "range_km": realtime.endurance_mileage or realtime.ev_endurance,
                "charging_state": getattr(realtime.effective_charging_state, "name", None),
                "vin": vin,
            }
    except Exception as e:
        print(f"Error querying BYD API via pyBYD: {e}")
        return None

def main():
    """Main monitoring logic."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    snapshot = asyncio.run(get_battery_snapshot())

    if snapshot is None:
        print(f"[{timestamp}] ERROR: Unable to retrieve battery level")
        return

    battery_level = snapshot["battery_level"]
    print(f"[{timestamp}] Battery level: {battery_level}%")
    print(json.dumps(snapshot, ensure_ascii=False))

    if battery_level < BATTERY_THRESHOLD:
        print(
            json.dumps(
                {
                    "alert": True,
                    "battery_level": battery_level,
                    "threshold": BATTERY_THRESHOLD,
                    "range_km": snapshot.get("range_km"),
                    "charging_state": snapshot.get("charging_state"),
                    "vin": snapshot.get("vin"),
                    "timestamp": timestamp,
                },
                ensure_ascii=False,
            )
        )
    else:
        print(f"[{timestamp}] Battery OK ({battery_level}% >= {BATTERY_THRESHOLD}%)")

if __name__ == "__main__":
    main()


path: scripts/byd_common.py
content: #!/usr/bin/env python3
"""Shared helpers for portable BYD Python scripts."""
from __future__ import annotations

import os
from pathlib import Path

from pybyd import BydClient, BydConfig


SCRIPT_DIR = Path(__file__).resolve().parent
ENV_PATH = SCRIPT_DIR / ".env"
VEHICLE_VIN_ENV = "BYD_VIN"
VEHICLE_ALIAS_ENV = "BYD_VEHICLE_ALIAS"


def load_env_file(path: Path = ENV_PATH) -> None:
    """Load KEY=VALUE pairs from a .env file into os.environ (no overwrite)."""
    if not path.is_file():
        return
    for raw in path.read_text().splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, _, value = line.partition("=")
        key = key.strip()
        value = value.strip().strip('"').strip("'")
        if key and key not in os.environ:
            os.environ[key] = value


def prepare_byd_env() -> None:
    """Load env vars and bridge BYD_PIN to BYD_CONTROL_PIN when needed."""
    load_env_file()
    if "BYD_PIN" in os.environ and "BYD_CONTROL_PIN" not in os.environ:
        os.environ["BYD_CONTROL_PIN"] = os.environ["BYD_PIN"]


def create_config() -> BydConfig:
    """Create pyBYD config from environment."""
    prepare_byd_env()
    return BydConfig.from_env()


async def get_target_vehicle(client: BydClient):
    """Return a selected vehicle using BYD_VIN or BYD_VEHICLE_ALIAS when provided."""
    vehicles = await client.get_vehicles()
    if not vehicles:
        raise RuntimeError("No vehicles tied to this account")

    wanted_vin = os.environ.get(VEHICLE_VIN_ENV, "").strip()
    if wanted_vin:
        for vehicle in vehicles:
            if vehicle.vin == wanted_vin:
                return vehicle
        raise RuntimeError(f"No vehicle found for {VEHICLE_VIN_ENV}={wanted_vin}")

    wanted_alias = os.environ.get(VEHICLE_ALIAS_ENV, "").strip().lower()
    if wanted_alias:
        for vehicle in vehicles:
            alias = (vehicle.auto_alias or "").strip().lower()
            if alias == wanted_alias:
                return vehicle
        raise RuntimeError(f"No vehicle found for {VEHICLE_ALIAS_ENV}={wanted_alias}")

    return vehicles[0]


path: scripts/flash_lights.py
content: #!/usr/bin/env python3
"""Flash lights + horn via pybyd."""
from __future__ import annotations

import asyncio

from pybyd import BydClient

from byd_common import create_config, get_target_vehicle


async def main() -> None:
    config = create_config()
    async with BydClient(config) as client:
        vehicle = await get_target_vehicle(client)
        vin = vehicle.vin
        await client.verify_command_access(vin)
        result = await client.flash_lights(vin)
        print(result.model_dump())


if __name__ == "__main__":
    asyncio.run(main())


path: scripts/lock_car.py
content: #!/usr/bin/env python3
"""Lock the vehicle via pybyd."""
from __future__ import annotations

import asyncio

from pybyd import BydClient

from byd_common import create_config, get_target_vehicle


async def main() -> None:
    config = create_config()
    async with BydClient(config) as client:
        vehicle = await get_target_vehicle(client)
        vin = vehicle.vin
        await client.verify_command_access(vin)
        result = await client.lock(vin)
        print(result.model_dump())


if __name__ == "__main__":
    asyncio.run(main())


path: scripts/start_ac.py
content: #!/usr/bin/env python3
"""Start BYD A/C remotely using the pyBYD library's minimal supported path."""

import asyncio

from pybyd._constants import minutes_to_time_span
from pybyd.client import BydClient
from pybyd.models.control import ClimateStartParams

from byd_common import create_config, get_target_vehicle


async def main():
    config = create_config()
    async with BydClient(config) as client:
        print("🔐 Logging in...")
        await client.login()

        print("🚗 Fetching vehicles...")
        try:
            vehicle = await get_target_vehicle(client)
        except RuntimeError:
            print("❌ No vehicles found!")
            return

        vin = vehicle.vin
        print(f"✅ Found: {vehicle.auto_alias or 'BYD M6'} (VIN: {vin})")

        try:
            await client.verify_command_access(vin)
            print("✅ Command access verified")
        except Exception as e:
            print(f"⚠️ Could not verify command access: {e}")

        # Match the library's documented happy path.
        # 20 minutes => time_span 3.
        hvac_params = ClimateStartParams(
            temperature=21.0,
            time_span=minutes_to_time_span(20),
        )

        print("🌬️ Starting A/C...")
        result = await client.start_climate(vin=vin, params=hvac_params)

        message = getattr(result, "message", None)
        success = getattr(result, "success", None)
        control_state = getattr(result, "control_state", None)
        res = getattr(result, "res", None)

        print("📦 Raw response:", result)

        if success is True or control_state == 1:
            print("✅ A/C started successfully!")
            if message:
                print(f"   Status: {message}")
            return

        print("⚠️ A/C command did not report success.")
        if message:
            print(f"   Message: {message}")
        if res is not None:
            print(f"   Result code: {res}")
        if control_state is not None:
            print(f"   Control state: {control_state}")


if __name__ == "__main__":
    asyncio.run(main())


path: scripts/stop_ac.py
content: #!/usr/bin/env python3
"""Stop BYD A/C remotely using pyBYD."""

import asyncio

from pybyd.client import BydClient

from byd_common import create_config, get_target_vehicle


async def main():
    config = create_config()
    async with BydClient(config) as client:
        print("🔐 Logging in...")
        await client.login()

        print("🚗 Fetching vehicles...")
        try:
            vehicle = await get_target_vehicle(client)
        except RuntimeError:
            print("❌ No vehicles found!")
            return

        vin = vehicle.vin
        print(f"✅ Found: {vehicle.auto_alias or 'BYD M6'} (VIN: {vin})")

        try:
            await client.verify_command_access(vin)
            print("✅ Command access verified")
        except Exception as e:
            print(f"⚠️ Could not verify command access: {e}")

        print("🛑 Stopping A/C...")
        result = await client.stop_climate(vin=vin)

        message = getattr(result, "message", None)
        success = getattr(result, "success", None)
        control_state = getattr(result, "control_state", None)
        res = getattr(result, "res", None)

        print("📦 Raw response:", result)

        if success is True or control_state == 1:
            print("✅ A/C stopped successfully!")
            if message:
                print(f"   Status: {message}")
            return

        print("⚠️ A/C stop command did not report success.")
        if message:
            print(f"   Message: {message}")
        if res is not None:
            print(f"   Result code: {res}")
        if control_state is not None:
            print(f"   Control state: {control_state}")


if __name__ == "__main__":
    asyncio.run(main())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)