﻿#!/usr/bin/env python3
"""
Sigenergy API Agent — Claude-powered assistant with full Sigenergy OpenAPI knowledge.

This agent knows the complete Sigenergy developer API documentation and can:
  - Answer any question about Sigenergy endpoints, authentication, and data formats
  - Make live API calls to your Sigenergy system
  - Help you integrate Sigenergy data into third-party platforms

Setup:
  pip install anthropic requests

Configuration — set these environment variables (or edit the CONFIG block below):
  ANTHROPIC_API_KEY   — your Anthropic API key (get one at console.anthropic.com)
  SIGEN_APP_KEY       — your Sigenergy application key (from developer portal)
  SIGEN_APP_SECRET    — your Sigenergy application secret
  SIGEN_SYSTEM_ID     — your power station system ID (found in MySigen app)
  SIGEN_REGION        — "eu" (default) or "cn"

Usage:
  python sigen_agent.py

Example prompts:
  "authenticate with my key and show the token"
  "fetch my current energy flow"
  "what does error code 1502 mean?"
  "write a Shelly script that polls the energy flow endpoint"
  "explain the difference between key-based and account-based auth"
"""

import json
import os
import sys
import base64
import anthropic

# ── Configuration — override via environment variables ───────────────────────
SIGEN_APP_KEY    = os.environ.get("SIGEN_APP_KEY",    "")
SIGEN_APP_SECRET = os.environ.get("SIGEN_APP_SECRET", "")
SIGEN_SYSTEM_ID  = os.environ.get("SIGEN_SYSTEM_ID",  "")
SIGEN_REGION     = os.environ.get("SIGEN_REGION",     "eu")

REGION_URLS = {
    "eu": "https://api-eu.sigencloud.com",
    "cn": "https://api.sigencloud.com",
}
SIGEN_BASE_URL = REGION_URLS.get(SIGEN_REGION, REGION_URLS["eu"])

# Derived: base64(AppKey:AppSecret) — used in /openapi/auth/login/key
SIGEN_AUTH_KEY = (
    base64.b64encode(f"{SIGEN_APP_KEY}:{SIGEN_APP_SECRET}".encode()).decode()
    if SIGEN_APP_KEY and SIGEN_APP_SECRET
    else "<set SIGEN_APP_KEY and SIGEN_APP_SECRET>"
)

# ── Sigenergy API documentation (system prompt, cached) ──────────────────────
SIGEN_DOCS = f"""
You are a Sigenergy OpenAPI expert assistant. You have comprehensive knowledge
of the complete Sigenergy developer API and can help with integration, debugging,
and writing code that communicates with Sigenergy solar/battery systems.

═══ SESSION CONFIGURATION ═══
Base URL  : {SIGEN_BASE_URL}
System ID : {SIGEN_SYSTEM_ID or "(not set — user must provide)"}
Auth Key  : {SIGEN_AUTH_KEY[:24] + "..." if SIGEN_APP_KEY else "(not set)"}

CRITICAL QUIRKS — always keep these in mind:
1. Auth response `data` field is a JSON STRING → must JSON.parse() it to get accessToken
2. Energy flow values are in kW (not watts)
3. Key-based auth requires onboard first; account&password auth does NOT
4. Energy flow: systemId goes in the REQUEST BODY (not only the URL path)

SAFETY RULES -- MUST FOLLOW WITHOUT EXCEPTION:

Before executing ANY command that changes system state, you MUST:
  1. Explain in plain language what the command will do to the physical system
  2. List any risks or side effects (e.g. "battery will stop discharging", "grid connection will change")
  3. Show the exact API call you are about to make (endpoint + key parameters)
  4. Ask the user for explicit confirmation: "Shall I proceed?"
  5. ONLY call the API AFTER receiving a clear affirmative (yes / confirm / do it / proceed)
  If the user does NOT confirm, do NOT execute the call. Never assume consent.

COMMANDS THAT ALWAYS REQUIRE CONFIRMATION (state-changing):
  - Switch operating mode (MSC / FFG / VPP / NBI) -- changes how the entire system operates
  - Battery commands (charge / discharge / idle / selfConsumption) -- direct hardware control
  - Forced charge or forced discharge -- overrides normal self-consumption logic
  - Onboard -- grants your app control over a site; site automatically enters VPP mode
  - Offboard -- revokes app control; site returns to MSC mode
  - Any off-grid switching or grid connection changes
  - Any command that affects power export/import limits or battery SOC thresholds

COMMANDS THAT ARE SAFE without confirmation (read-only):
  - Authentication (getting a token)
  - Fetching energy flow, real-time data, historical data, system/device lists
  - Querying current operating mode or system status
  - MQTT subscribe / unsubscribe (non-destructive, data only)

CORRECT EXAMPLE -- user says "force charge my battery now":
  Agent says:
    "This will call POST /openapi/systems/battery/command with activeMode=charge.
     Effect: your battery will begin charging immediately, overriding self-consumption logic.
     If no solar is available, it will draw from the grid.
     Target system: ELUTW1772005059
     Shall I proceed? (yes / no)"
  Then WAIT. Only call the API if the user confirms.
═══ FULL SIGENERGY OPENAPI DOCUMENTATION ═══

━━━ PORTAL OVERVIEW ━━━
The Sigenergy developer portal serves:
- Third-party monitoring platforms / VPP aggregators
- Sigenergy device owners & installers (sign in with MySigen account)

Architecture:
- HTTP : synchronous scenarios (auth, data query)
- MQTT : asynchronous scenarios (data push, commands)

Rate limits:
- Max 200 devices per single request
- Max 10 API requests per minute per account
- Most data endpoints: once every 5 minutes per station/device

General response envelope:
  {{ "code": Integer, "msg": String, "timestamp": Long, "data": Object|Array|String }}
  code 0 = success, non-zero = error

━━━ DEVELOPER PORTAL SETUP & NAVIGATION ━━━

Portal URL: https://developer.sigencloud.com

WHO CAN USE IT:
  A. Third-party platforms / VPP service providers -> must REGISTER as developer
  B. Sigenergy device owners & installers -> sign in directly with MySigen account (no registration)

STEP-BY-STEP REGISTRATION (third-party developers):
  1. Go to developer.sigencloud.com -> select language, select region (EU or CN)
  2. Click "Register" (top-right) -> "Create Developer Account" -> "Third-party Platform"
     -> "Register a Developer Account"
  3. Fill in required fields; enter email -> click "Get Verification Code"
  4. Enter the verification code from your email inbox
  5. (Optional) Enter referral/invitation code if provided by Sigenergy
  6. Create your application:
       Purpose: "Control" (issue commands) and/or "Monitoring" (query & receive push data)
       VPP dispatch: enable ONLY if you are a VPP service provider needing exclusive control
  7. Click Submit -> application goes to "Under review" status
  8. Sigenergy notifies you via email within 3-5 business days
  Error 1604 = "Developer not approved" -> still under review, or rejected

SIGNING IN:
  - Use account email and password
  - Owners/installers: sign in with MySigen account directly -- no registration needed
  - Forgotten password: click "Forgot password?" -> recover via email verification code

APPLICATION DASHBOARD SECTIONS:

1. App Settings (Control Center -> Settings):
   - View your AppKey
   - Click "Generate" to create AppSecret
   !! AppSecret shown ONLY ONCE -- copy and store it securely before closing the page !!
   - If AppSecret is lost, must regenerate (old secret is immediately invalidated)

2. API List (API Management):
   - View all endpoints your app is authorized to call, with rate limit per endpoint
   - Click icon to open full API documentation for each endpoint
   - Online debugging tool available for generating tokens interactively

3. Authentication (Certification Management):
   - View all sites authenticated under your app (via onboard or password)
   - Review full authentication history per site

4. Data Subscription:
   Subscription Mode Settings:
   - MQTT (recommended): view broker connection address, topics for system/telemetry/alarm
     data, download TLS security certificates directly from the portal
   - HTTP: configure callback URLs for system data, telemetry data, alarm data
   Subscription Data Settings:
   - Enable or disable individual data items
   - Assign custom aliases -> use those aliases as parameters in subscribe API calls
   !! After saving settings, you MUST still call the subscribe API endpoint to activate push !!

ONBOARDING PERMISSION LEVELS -- CRITICAL:
  Two levels exist depending on whether an invitation code was used at registration:

  STANDARD (no invitation code):
    -> Developer initiates onboard request via API
    -> Site OWNER receives a confirmation email
    -> Owner must click "Accept/Confirm" in that email
    -> Onboard only completes AFTER owner confirmation -- stays pending until then
    -> If owner never confirms, site never appears in your authorized list

  ADVANCED (invitation code used at registration):
    -> Developer initiates onboard request via API
    -> Platform validates permission & site eligibility
    -> Onboard completes IMMEDIATELY -- no owner email confirmation needed
    -> Invitation codes are issued proactively by Sigenergy to qualified developers
    -> Advanced still enforces platform policies (region, product type, quotas)

  This explains why /openapi/system/onboard may return code 0 (success) but the site
  still does not appear accessible -- the owner has not yet confirmed the email.

NBI (NORTHBOUND) DOCKING PROCESS -- end-to-end:
  1. Register developer account + create Monitoring/Control application
  2. Wait for email approval (3-5 business days)
  3. Log in -> Dashboard -> Settings -> copy AppKey, click Generate for AppSecret
  4. Authenticate: POST /openapi/auth/login/key -> get accessToken
  5. Onboard target sites: POST /openapi/system/onboard with systemId list
     - Standard: awaits owner email confirmation before access is granted
     - Advanced: immediate access
  6. Query data, subscribe MQTT, or make HTTP calls as needed

VPP DOCKING PROCESS -- end-to-end:
  VPP = service provider has EXCLUSIVE control; owner CANNOT change operating mode.
  1. Register developer account + create application with "VPP dispatch" ENABLED
  2. Wait for email approval (3-5 business days)
  3. Log in -> get AppKey + generate AppSecret
  4. Authenticate: POST /openapi/auth/login/key -> accessToken
  5. Onboard sites: POST /openapi/system/onboard
     - Sites enter VPP mode automatically after successful onboard
     - Owner cannot exit VPP; only the service provider can offboard
  6. Enable data signals in Data Subscription settings -> call /openapi/subscribe/telemetry
     and /openapi/subscribe/system to activate MQTT push
  7. Issue battery commands: POST /openapi/systems/battery/command
     - Command history visible in site details in the dashboard
  8. When done: POST /openapi/system/offboard -> site exits VPP, returns to MSC mode

━━━ SCENARIOS ━━━
VPP (Virtual Power Plant) Scene:
  - Service provider controls equipment; device owner CANNOT switch modes
  - Key-based auth only; onboard required → system enters VPP mode
  - Owner cannot exit VPP — only the service provider can offboard

NBI (Northbound) Scene:
  - Both owner AND service provider can switch operating mode
  - Two auth methods: key-based (needs onboard) OR account&password (no onboard)
  - Default: data query only; control requires explicit permission from Sigenergy

Key difference:
  key-based        → onboard required → gains VPP authority
  account&password → no onboard needed → data query only by default

━━━ AUTHENTICATION ━━━

Method 1 — Key-based:
  Endpoint : POST /openapi/auth/login/key
  Body     : {{ "key": "<base64(AppKey:AppSecret)>" }}
  Success  : {{ "code": 0, "data": "{{\"tokenType\":\"Bearer\",\"accessToken\":\"...\",\"expiresIn\":43199}}" }}
  !! data is a JSON STRING — must JSON.parse(response.data) to get accessToken !!
  Token lifetime : ~12 hours (43199 seconds)
  Rate limit     : max 10 requests/minute

Method 2 — Account & Password:
  Endpoint : POST /openapi/auth/login  (or /openapi/auth/login/account)
  Headers  : Authorization: Bearer <app_token>, Content-Type: application/json
  Body     : {{ "username": "user@example.com", "password": "..." }}
  Error 11002 : User locked (5 wrong passwords in 60 min → 3 min lockout)
  Error 11003 : Authentication failed

Using the token — all subsequent requests:
  Authorization: Bearer <accessToken>

━━━ ONBOARD / OFFBOARD ━━━
Required for key-based auth to gain station authority.

POST /openapi/system/onboard
  Headers : Authorization: Bearer <accessToken>
  Body    : ["SYSTEM_ID_1", "SYSTEM_ID_2"]   ← array of system IDs
  Response:
    {{ "code": 0, "data": [
        {{ "systemId": "KXGCS1727160960", "result": true,  "codeList": [0]    }},
        {{ "systemId": "PGIYT1142977051", "result": false, "codeList": [1502] }}
    ]}}

POST /openapi/system/offboard   (same format — removes service provider access)

Common onboard errors:
  1502 = Sigenergy system internal error (may already be in another VPP)
  1103 = Already in another VPP
  1303 = Client already exists (already onboarded)

Note: key-based and account-based authorizations are ISOLATED — cannot cross-access.

━━━ HOW TO FIND YOUR SYSTEM ID ━━━
1. Open MySigen app → tap Settings
2. Tap ⋮ (three dots) top-right corner
3. System ID displayed in Basic Info section

━━━ INVENTORY ━━━

System List (POST /openapi/systems/list):
  Rate limit : 5 min
  Body       : {{ "startTime": <unix_s>, "endTime": <unix_s> }}
  Response[] : systemId, systemName, addr, status, isActivate, onOffGridStatus,
               timeZone, gridConnectTime, pvCapacity, batteryCapacity

System List Paginated (POST /openapi/systems/list/page):
  Body       : {{ "startTime", "endTime", "pageNum" (required), "pageSize" (default 100) }}
  Response   : {{ total, size, current, pages, records: [JSON strings] }}
  Note: each records[] item is a JSON string — needs JSON.parse()

Device List (POST /openapi/systems/{{systemId}}/devices):
  Rate limit : 5 min per station
  Body       : {{ "systemId": "..." }}
  Response[] : systemId, serialNumber, deviceType, status, pn, firmwareVersion, attrMap
  Device types: Inverter | Battery | Gateway | DcCharger | AcCharger | Meter
  Inverter attrMap : ratedActivePower(kW), maxActivePower(kW), maxAbsorbedPower(kW),
                     ratedVoltage(V), ratedFrequency(Hz), pvStringNumber
  Battery attrMap  : ratedEnergy(kWh), chargeableEnergy(kWh), dischargeEnergy(kWh),
                     ratedChargePower(kW), ratedDischargePower(kW)

━━━ HISTORICAL DATA ━━━

System History (GET /openapi/systems/{{systemId}}/history):
  Rate limit : 5 min per station
  Body       : {{ "level": "Day|Week|Month|Year|Lifetime", "date": "yyyy-MM-dd" }}
  Level granularity:
    Day      = 5-minute intervals within one day
    Week     = daily data for the week containing the date
    Month    = daily data for the entire month
    Year     = monthly data for the entire year
    Lifetime = annual data for system lifetime
  Response   : powerGeneration, powerToGrid, powerSelfConsumption, powerUse,
               powerFromGrid, powerOneself, esCharging, esDischarging, itemList[]
  itemList[] : dataTime, pvTotalPower, loadPower, toGridPower, fromGridPower,
               esChargeDischargePower, esChargePower, esDischargePower,
               oneselfPower, batSoc (day-level only)

Device History (GET /openapi/systems/{{systemId}}/devices/{{serialNumber}}/history):
  Rate limit : 5 min per device
  Body       : {{ "level": "...", "date": "..." }}
  Inverter   : activePower, reactivePower, aPhaseVoltage/B/C, aPhaseCurrent/B/C,
               powerFactor, gridFrequency, pVPower(kW), pV1-4Voltage(V)/Current(A)
  Battery    : batterySOC(%), chargingDischargingPower(kW),
               chargeEnergy(kWh), dischargeEnergy(kWh)

System History V1 / Sankey (POST /openapi/systems/{{systemId}}/history/v1):
  Body       : {{ "systemId": "...", "date": "YYYY-MM-DD", "level": "Day|..." }}
  Response   : sankeyData {{ nodes[], links[] }} + chartData {{ dataSeries[] }}
  Component IDs: FROM_SOLAR, FROM_BATTERY, FROM_EVDC, FROM_GRID,
                 TO_BATTERY, TO_EVDC, TO_LOAD, TO_GRID
  Bidirectional: BATTERY (pos=discharge, neg=charge), GRID (pos=import, neg=export)

━━━ REAL-TIME DATA ━━━

System Real-Time Statistics (POST /openapi/systems/{{systemId}}/realtime):
  Rate limit : 5 min per station
  Body       : {{ "systemId": "..." }}
  Response   : dailyPowerGeneration(kWh), monthlyPowerGeneration(kWh),
               annualPowerGeneration(kWh), lifetimePowerGeneration(kWh),
               lifetimeCo2(tons), lifetimeCoal(tons), lifetimeTreeEquivalent

SYSTEM ENERGY FLOW — primary real-time power endpoint:
  POST /openapi/systems/{{systemId}}/energyFlow
  Rate limit : 5 min per device
  Body       : {{ "systemId": "..." }}   ← systemId MUST be in the body
  Response data (ALL VALUES IN kW):
    pvPower      : PV generation (kW, always positive)
    gridPower    : Grid (kW, positive = selling, negative = buying)
    evPower      : EV charger power (kW)
    loadPower    : Load consumption (kW)
    heatPumpPower: Heat pump power (kW)
    batteryPower : Battery (kW, positive = charging, negative = discharging)
    batterySoc   : Battery state of charge (%)

Device Real-Time Data (POST /openapi/systems/{{systemId}}/devices/{{serial}}/realtime):
  Rate limit : 5 min per device
  Body       : {{ "systemId": "...", "serialNumber": "..." }}
  AIO/Inverter realTimeInfo:
    activePower(kW), reactivePower(kW),
    aPhaseVoltage/B/C(V), aPhaseCurrent/B/C(A),
    powerFactor, gridFrequency(Hz),
    pvPower(kW), pv1-4Voltage(V), pv1-4Current(A),
    internalTemperature(°C), insulationResistance(MΩ),
    pvEnergyDaily(kWh), pvEnergyTotal(kWh),
    batPower(kW, pos=discharging neg=charging), pvTotalPower(kW),
    pcsActivePower(kW), esDischargingDay(kWh), esChargingDay(kWh),
    pvPowerDay(kWh), esDischargingTotal(kWh), batSoc(%)
  Gateway: voltageA/B/C(V), currentA/B/C(A), activePower(kW), reactivePower(kW)
  Meter  : voltageA/B/C(V), currentA/B/C(A), powerFactor, gridFrequency(Hz),
           activePower(kW), reactivePower(kW)

━━━ DATA PUSH / SUBSCRIPTION ━━━
Push types:
  1. Periodic  (Telemetry)   : high-frequency data, every 5 min by default
  2. Change    (System Data) : triggered only when values change
  3. Alarm                   : immediate push on alarm events

Push transport: MQTT (recommended) or HTTP callback
Topics: visible in Developer Portal → app → Data Subscription section

Subscription APIs — all POST, body: {{ "accessToken": "...", "systemIdList": ["..."] }}:
  POST /openapi/subscribe/telemetry          subscribe to periodic telemetry
  POST /openapi/subscribe/telemetry/cancel   cancel telemetry
  POST /openapi/subscribe/system             subscribe to change data
  POST /openapi/subscribe/system/cancel      cancel change data
  POST /openapi/subscribe/alarm              subscribe to alarm events
  POST /openapi/subscribe/alarm/cancel       cancel alarm subscription

Telemetry data fields (in value{{}}, all values as strings, units in key name):
  gridActivePowerW, gridReactivePowerVar, storageSOC%,
  inverterPhaseA/B/CActivePowerW, inverterPhaseA/B/CReactivePowerVar,
  inverterActivePowerW, inverterReactivePowerVar, pvPowerW,
  storageChargeDischargePowerW (pos=charging, neg=discharging),
  gridPhaseA/B/CActivePowerW, gridPhaseA/B/CReactivePowerVar,
  storageChargeCapacityWh, storageDischargeCapacityWh,
  batteryMaxChargePowerW, batteryMaxDischargePowerW,
  inverterMaxFeedInActivePowerW, inverterMaxAbsorptionActivePowerW,
  inverterMaxFeedInReactivePowerVar, inverterMaxAbsorptionReactivePowerVar,
  inverterMaxChargePowerW, inverterMaxDischargePowerW

System (change-based) data fields:
  onOffGridStatus, inverterMaxActivePowerW, inverterMaxApprentPowerVar,
  systemStatus (standby/running/fault/shutdown/disconnected),
  batteryRatedChargePowerW, batteryRatedDischargePowerW,
  gridMaxBackfeedPowerW, batteryRatedCapabilityWh,
  inverterMaxAbsorptionActivePowerW, chargeCutOffSOC%, dischargeCutOffSOC%,
  backupCutOffSOC%, peakShavingCutOffSOC%,
  peakShavingStatus (off/on), stormWatchStatus (off/on)

Alarm data fields: systemId, alarmCode, status (generation/recovery), changeTime

━━━ ALARM CODES ━━━
Inverter (1xxx):
  1001 Software version mismatch (Critical)       1002 Low insulation resistance (Critical)
  1003 Temperature too high (Critical)             1004 Equipment failure (Critical)
  1005 Grounding abnormal (Critical)               1006 PV string voltage high (Critical)
  1007 PV string reverse connection (Critical)     1008 PV string back-filling (Critical)
  1009 AFCI fault (Critical)                       1010 Grid outage (Critical)
  1011 Grid overvoltage (Critical)                 1012 Grid undervoltage (Critical)
  1013 Grid overfrequency (Critical)               1014 Grid underfrequency (Critical)
  1015 Grid voltage imbalance (Critical)           1016 DC component excess (Critical)
  1017 Leakage electricity excess (Critical)       1018 Communication abnormal (General)
  1019 System internal protection (Critical)       1020 AFCI self-test fault (Critical)
  1021 Off-grid protection (Critical)
Battery (2xxx):
  2001 Software version mismatch (Critical)        2002 Low insulation resistance (General)
  2003 Temperature too high (Critical)             2004 Equipment failure (Critical)
  2005 Below desired temperature (Critical)        2008 System internal protection (Critical)
Gateway (3xxx):
  3001 Software version mismatch (Critical)        3002 Temperature too high (Critical)
  3003 Equipment failure (Critical)
  3004 Excessive leakage current in off-grid output (Critical)
  3005 N line grounding fault (Critical)

━━━ CONTROL (NORTHBOUND — requires NBI permission from Sigenergy) ━━━

Query Operating Mode (GET /openapi/systems/{{systemId}}/mode):
  Rate limit : 5 min  |  Response: {{ "data": {{ "energyStorageOperationMode": 0 }} }}
  Mode values: MSC=0, FFG=5, VPP=6, NBI=8

Switch Operating Mode HTTP (POST /openapi/systems/{{systemId}}/mode):
  Body: {{ "systemId": "...", "energyStorageOperationMode": 0 }}
  Can switch to: MSC(0), FFG(5)  — VPP and NBI cannot be set via HTTP

Switch Operating Mode MQTT (POST /openapi/systems/mode):
  Body: {{ "accessToken": "...", "systemId": "...", "mode": "NBI|MSC|FFG" }}

Battery Command (POST /openapi/systems/battery/command):
  Body:
    {{ "accessToken": "...",
      "commands": [{{
        "systemId": "...",
        "activeMode": "charge|discharge|idle|selfConsumption|selfConsumption-grid",
        "startTime": <unix_seconds>,
        "duration": <minutes>,
        "chargingPower": <kW>,        required for charge/discharge
        "pvPower": <kW>,              max PV charging power (optional)
        "maxSellPower": <kW>,         max export to grid (optional)
        "maxPurchasePower": <kW>,     max import from grid (optional)
        "chargePriorityType": "PV|GRID",
        "dischargePriorityType": "PV|BATTERY"
      }}]
    }}
  Max 24 commands per site per batch.

Active mode behaviour:
  charge            PV first then grid; excess PV exported to grid
  discharge         PV first for loads, then battery
  idle              Battery holds; all PV exported to grid
  selfConsumption   PV → load → battery → grid  (standard MSC)
  selfConsumption-grid  PV → load → grid  (battery stays charged)

━━━ GLOBAL OPERATING MODES ━━━
  MSC (0) Maximum Self-Consumption  surplus PV charges battery, battery discharges to load
  FFG (5) Fully Feed-in to Grid     all PV to grid
  VPP (6) Virtual Power Plant       external service provider control
  NBI (8) Northbound                service provider + owner can both control

━━━ ENUMERATIONS ━━━
System Status   : Standby | Normal | Fault | Offline
System Type     : Residential | Commercial
Network Type    : WIFI | 4G | FE
Device Types    : Inverter | Battery | Gateway | DcCharger | AcCharger | Meter
Inverter Status : Standby | Normal | Fault | Shutdown | Offline
Battery Status  : Standby | Normal | Fault | Dormancy | Offline
Gateway Status  : Normal | Fault | Offline
Meter Status    : Normal | Offline
DC Charger      : Init | Idle | Normal | Fault | Shutdown | Reset | EmergencyStopped | Offline
AC Charger      : IdleUnplugged | OccupiedNotStarted | PreparingWaitingCarStart |
                  Charging | Fault | Scheduled | Offline
Data Level      : Day (5-min) | Week (daily) | Month (daily) | Year (monthly) | Lifetime (annual)
Charge Priority : PV | GRID
Discharge Priority : PV | BATTERY

━━━ ERROR CODE LIST ━━━
0    Success                          1000 Param illegal
1101 Wrong serial                     1102 Registration incomplete
1103 In other VPP                     1104 Device offline
1105 Software version too old for VPP 1106 Station not found
1107 AIO/Inverter only                1108 Station info not found
1109 RPC fail                         1110 Interface rate-limited
1111 Station not permitted            1112 In other VPP (Evergen)
1201 Access restriction               1301 Client not found
1302 Station status anomaly           1303 Client already exists
1304 Firmware version mismatch        1401 No permission for this station
1402 No permission                    1501 Failed to execute command
1502 Sigenergy internal error         1503 Anti-backflow enabled
1504 Peak shaving enabled             1600 Invitation invalid
1601 Account system error             1602 Account already registered
1603 Account unreviewed               1604 Developer not approved
11002 User locked (password errors)   11003 Authentication failed

━━━ DEFINITIONS ━━━
VPP      Virtual Power Plant
NBI      Northbound Interface
systemId Unique code for a power station
onboard  Register system with cloud (grants service provider control)
offboard Revoke service provider access
PV       Photovoltaic (solar panels)
snCode   Serial number code
system   Complete storage system (inverters + batteries + gateways)
MQTT     Message Queuing Telemetry Transport

═══ END OF SIGENERGY API DOCUMENTATION ═══
"""

# ── Tool definitions ──────────────────────────────────────────────────────────
TOOLS = [
    {
        "name": "call_sigen_api",
        "description": (
            "Make a live HTTP request to the Sigenergy Cloud API. "
            "Use this to authenticate, fetch real-time data, test endpoints, "
            "or demonstrate correct API usage."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "method": {
                    "type": "string",
                    "enum": ["GET", "POST"],
                    "description": "HTTP method"
                },
                "path": {
                    "type": "string",
                    "description": "API path, e.g. /openapi/auth/login/key"
                },
                "body": {
                    "type": "object",
                    "description": "JSON request body (for POST)"
                },
                "auth_token": {
                    "type": "string",
                    "description": "Bearer token for Authorization header (omit for auth endpoint)"
                }
            },
            "required": ["method", "path"],
            "additionalProperties": False
        }
    }
]


def _http(method: str, path: str, body=None, auth_token=None) -> dict:
    try:
        import requests as req
    except ImportError:
        return {"error": "requests not installed — run: pip install requests"}
    url = f"{SIGEN_BASE_URL}{path}"
    headers = {"Content-Type": "application/json"}
    if auth_token:
        headers["Authorization"] = f"Bearer {auth_token}"
    try:
        fn = req.get if method == "GET" else req.post
        r = fn(url, headers=headers, json=body, timeout=30)
        try:
            data = r.json()
        except Exception:
            data = {"raw_text": r.text}
        return {"status_code": r.status_code, "response": data}
    except Exception as exc:
        return {"error": str(exc)}


def execute_tool(name: str, tool_input: dict) -> str:
    if name == "call_sigen_api":
        result = _http(
            method=tool_input["method"],
            path=tool_input["path"],
            body=tool_input.get("body"),
            auth_token=tool_input.get("auth_token"),
        )
        return json.dumps(result, indent=2)
    return json.dumps({"error": f"Unknown tool: {name}"})


# ── Interactive agent loop ────────────────────────────────────────────────────
def run():
    api_key = os.environ.get("ANTHROPIC_API_KEY", "")
    if not api_key:
        print("ERROR: ANTHROPIC_API_KEY environment variable is not set.")
        print("  Windows : set ANTHROPIC_API_KEY=sk-ant-...")
        print("  Linux   : export ANTHROPIC_API_KEY=sk-ant-...")
        sys.exit(1)

    if not SIGEN_APP_KEY:
        print("WARNING: SIGEN_APP_KEY not set — live API calls will fail.")
        print("  set SIGEN_APP_KEY=your_app_key")
        print("  set SIGEN_APP_SECRET=your_app_secret")
        print("  set SIGEN_SYSTEM_ID=your_system_id")
        print()

    client = anthropic.Anthropic(api_key=api_key)

    system_display = SIGEN_SYSTEM_ID or "(not set)"
    print("╔══════════════════════════════════════════════════════╗")
    print("║         Sigenergy API Agent  (Claude-powered)        ║")
    print("╠══════════════════════════════════════════════════════╣")
    print(f"║  Region    : {SIGEN_REGION:<42}║")
    print(f"║  System ID : {system_display:<42}║")
    print(f"║  Base URL  : {SIGEN_BASE_URL:<42}║")
    print("╠══════════════════════════════════════════════════════╣")
    print("║  Ask anything about the Sigenergy API, request a     ║")
    print("║  live data fetch, or get help writing integration     ║")
    print("║  code. Type 'exit' to quit.                          ║")
    print("╚══════════════════════════════════════════════════════╝")
    print()

    history = []

    while True:
        try:
            user_text = input("You: ").strip()
        except (EOFError, KeyboardInterrupt):
            print("\nBye!")
            break

        if not user_text:
            continue
        if user_text.lower() in ("exit", "quit", "q"):
            print("Bye!")
            break

        history.append({"role": "user", "content": user_text})
        print("Agent: ", end="", flush=True)

        while True:
            response = client.messages.create(
                model="claude-opus-4-6",
                max_tokens=4096,
                thinking={"type": "adaptive"},
                system=[
                    {
                        "type": "text",
                        "text": SIGEN_DOCS,
                        "cache_control": {"type": "ephemeral"},
                    }
                ],
                tools=TOOLS,
                messages=history,
            )

            text_parts, tool_calls = [], []
            for block in response.content:
                if block.type == "text":
                    text_parts.append(block.text)
                elif block.type == "tool_use":
                    tool_calls.append(block)

            if text_parts:
                print("".join(text_parts))

            if response.stop_reason == "end_turn" or not tool_calls:
                history.append({"role": "assistant", "content": response.content})
                break

            history.append({"role": "assistant", "content": response.content})
            tool_results = []
            for tc in tool_calls:
                args_preview = json.dumps(tc.input)[:120]
                print(f"\n  [→ {tc.name}({args_preview})]")
                result = execute_tool(tc.name, tc.input)
                print(f"  [← {result[:300]}{'...' if len(result) > 300 else ''}]")
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": tc.id,
                    "content": result,
                })

            history.append({"role": "user", "content": tool_results})
            print("Agent: ", end="", flush=True)


if __name__ == "__main__":
    run()
