Skill: zoranjurcevic/podpoint (version 1.0.0)

# Pod Point Watcher This skill monitors the live status of a specific Pod Point charging pod using Pod Point's public status endpoint. It mirrors the behaviour of a native Pod Point watcher: - Tracks connector **A** and **B** - Detects when a charger goes from **Charging → Available** - Detects when **both connectors become available** - Can either return current status or wait and notify when availability changes No authentication or API keys are required. --- ## When to use this skill Use this skill when the user asks things like: - "Is my Pod Point charger free?" - "Check pod 10059" - "Watch my charger and tell me when it's available" - "Are both connectors free at my Pod Point?" - "Monitor this Pod Point" This skill is specifically for **live availability**, not for maps or locations. --- ## Actions ### `podpoint_status` Returns the current state of connectors A and B. Example input: ```json { "action": "podpoint_status", "podId": "10059" }

path: index.js
content: const ENDPOINT = (podId) =>
  `https://charge.pod-point.com/ajax/pods/${encodeURIComponent(podId)}`;

const HEADERS = {
  "accept": "application/json,text/plain,*/*",
  "user-agent": "Mozilla/5.0"
};

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

function isAvailable(status) {
  if (!status) return false;
  const s = String(status).toLowerCase();
  return s.includes("available") || s.includes("free");
}

function isCharging(status) {
  if (!status) return false;
  return String(status).toLowerCase().includes("charging");
}

async function fetchStatuses(podId) {
  const res = await fetch(ENDPOINT(podId), {
    method: "GET",
    headers: HEADERS
  });

  if (!res.ok) {
    const t = await res.text().catch(() => "");
    throw new Error(`Pod Point fetch failed: ${res.status} ${res.statusText} ${t.slice(0, 200)}`);
  }

  const decoded = await res.json();
  const firstPod = decoded?.pods?.[0];
  if (!firstPod) throw new Error("No pods returned");

  const current = {};
  for (const st of firstPod.statuses || []) {
    if (st?.door) current[String(st.door)] = String(st.label ?? "");
  }

  return current;
}

function summarise(podId, current) {
  const statusA = current["A"] ?? "Unknown";
  const statusB = current["B"] ?? "Unknown";

  return {
    podId,
    statusA,
    statusB,
    availableA: isAvailable(statusA),
    availableB: isAvailable(statusB),
    bothAvailable: isAvailable(statusA) && isAvailable(statusB),
    raw: current
  };
}

async function podpoint_status({ podId }) {
  const current = await fetchStatuses(podId);
  return { ok: true, ...summarise(podId, current) };
}

async function podpoint_watch({ podId, intervalSeconds = 30, timeoutSeconds = 900 }) {
  const interval = Math.max(5, Number(intervalSeconds) || 30);
  const timeout = Math.max(interval, Number(timeoutSeconds) || 900);

  const started = Date.now();
  let previous = {};
  let initialNotified = false;

  while (Date.now() - started < timeout * 1000) {
    let current;
    try {
      current = await fetchStatuses(podId);
    } catch {
      await sleep(interval * 1000);
      continue;
    }

    const statusA = current["A"] ?? "Unknown";
    const statusB = current["B"] ?? "Unknown";
    const isInitialState = Object.keys(previous).length === 0;

    if (isInitialState && !initialNotified) {
      initialNotified = true;
      for (const door of ["A", "B"]) {
        if (isAvailable(current[door])) {
          return {
            ok: true,
            podId,
            statusA,
            statusB,
            event: {
              type: "initial_available",
              door,
              title: `Charger ${door} available`,
              body: `Connector ${door} at pod ${podId} is available.`
            }
          };
        }
      }
    }

    for (const door of ["A", "B"]) {
      const prev = previous[door];
      const now = current[door];
      if (isCharging(prev) && isAvailable(now)) {
        return {
          ok: true,
          podId,
          statusA,
          statusB,
          event: {
            type: "charging_to_available",
            door,
            title: `Charger ${door} available`,
            body: `Connector ${door} at pod ${podId} is now free.`,
            from: prev,
            to: now
          }
        };
      }
    }

    const bothNow = isAvailable(current["A"]) && isAvailable(current["B"]);
    const bothPrev = isAvailable(previous["A"]) && isAvailable(previous["B"]);

    if (!isInitialState && bothNow && !bothPrev) {
      return {
        ok: true,
        podId,
        statusA,
        statusB,
        event: {
          type: "both_available",
          title: "Both chargers available",
          body: `Connectors A and B at pod ${podId} are now free.`
        }
      };
    }

    previous = current;
    await sleep(interval * 1000);
  }

  const latest = await fetchStatuses(podId).catch(() => ({}));
  return { ok: true, timedOut: true, ...summarise(podId, latest) };
}

module.exports = async function run(input) {
  const action = input?.action || "podpoint_status";
  if (!input?.podId) return { ok: false, error: "podId is required" };

  if (action === "podpoint_status") return podpoint_status(input);
  if (action === "podpoint_watch") return podpoint_watch(input);

  return { ok: false, error: `Unknown action: ${action}` };
};

if (require.main === module) {
  const input = {
    action: process.argv[2] || "podpoint_status",
    podId: process.argv[3] || "10059",
    intervalSeconds: 15,
    timeoutSeconds: 300
  };

  module.exports(input)
    .then((res) => console.log(JSON.stringify(res, null, 2)))
    .catch((err) => {
      console.error(err);
      process.exit(1);
    });
}



path: skill.json
content: {
  "name": "podpoint",
  "title": "Pod Point watcher",
  "description": "Polls Pod Point pod status endpoint and reports connector A/B availability.",
  "version": "1.0.0",
  "entrypoint": "index.js",
  "inputs": {
    "type": "object",
    "properties": {
      "action": {
        "type": "string",
        "enum": ["podpoint_status", "podpoint_watch"],
        "default": "podpoint_status"
      },
      "podId": { "type": "string", "default": "10059" },
      "intervalSeconds": { "type": "number", "default": 30 },
      "timeoutSeconds": { "type": "number", "default": 900 }
    },
    "required": ["action", "podId"]
  }
}



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