Skill: jftech/jf-open-pro-feed-control (version 1.0.0)

--- name: jf-open-pro-feed-control description: 杰峰智能宠物喂食器设备控制技能（开发版）。支持远程一键喂食、设置或查询定时喂食计划，以及配置设备端的宠物检测功能开关。 metadata: version: 1.0.0 author: JFTech category: device tags: - 杰峰 - 宠物喂食器 - 智能喂食 - 定时喂食 - 宠物检测 - 一键喂食 triggers: - 查询设备是否支持喂食 - 一键喂食 - 远程喂食 - 定时喂食计划 - 查询喂食计划 - 设置喂食计划 - 宠物检测开关 - 查询宠物检测 - 开启宠物检测 - 关闭宠物检测 prerequisites: - 配置必需的环境变量 - 设备需已完成配网和绑定 - 设备需在线 region: - CN: api-cn.jftechws.com (中国大陆) - AS: api-as.jftechws.com (亚洲) - EU: api-eu.jftechws.com (欧洲) - NA: api-na.jftechws.com (北美洲) --- # jf-open-pro-feed-control - 杰峰智能宠物喂食器设备控制技能（开发版） ## 技能描述 支持杰峰智能宠物喂食器设备的远程控制功能： - **查询设备是否支持喂食** - 查询设备能力集，确认是否支持喂食功能 - **一键喂食** - 远程发送喂食指令，控制设备出粮 - **定时喂食计划** - 查询和设置设备的定时喂食计划 - **宠物检测** - 查询和设置设备端的宠物检测功能开关 **适用场景：** - 远程给宠物喂食 - 设置定时自动喂食计划 - 配置宠物检测自动触发喂食 ## 触发词 - 查询设备是否支持喂食 / 设备支持喂食吗 - 一键喂食 / 远程喂食 / 手动喂食 / 立即喂食 - 定时喂食计划 / 查询喂食计划 / 设置喂食计划 - 宠物检测开关 / 查询宠物检测 / 开启宠物检测 / 关闭宠物检测 ## 前置条件 ### 硬件要求 1. **智能宠物喂食器** - 设备需为杰峰智能宠物喂食器 ### 必需配置 1. **签名算法** - 使用杰峰官方移位加密算法生成 signature 2. **时间戳算法** - counter(7 位) + timeMillis(13 位)，实时生成 3. **设备绑定** - 设备需先绑定到开放平台账号 ### 环境变量 | 变量名 | 说明 | 默认值 | 必需 | |--------|------|--------|------| | `JF_UUID` | 开放平台用户 uuid | - | ✅ | | `JF_APP_KEY` | 开放平台应用 appKey | - | ✅ | | `JF_APP_SECRET` | 开放平台应用密钥 | - | ✅ | | `JF_MOVE_CARD` | 移动卡标识（用于签名） | `2` | ✅ | | `JF_DEVICE_SN` | 设备序列号 | - | ✅ | | `JF_DEVICE_TOKEN` | 设备接口访问令牌 | - | ✅ | | `JF_ENDPOINT` | API 接入地址 | `api-cn.jftechws.com` | ❌ | ## API 接口 | 功能 | 地址 | 方法 | 需要 Token | 需要在线 | |------|------|------|------------|----------| | 查询设备能力 | `POST /gwp/v3/rtc/device/getability/{token}` | POST | ✅ | ✅ | | 一键喂食 | `POST /gwp/v3/rtc/device/feeder/{token}` | POST | ✅ | ✅ | | 查询定时喂食计划 | `POST /gwp/v3/rtc/device/iotPropSet/{token}` | POST | ✅ | ✅ | | 设置定时喂食计划 | `POST /gwp/v3/rtc/device/iotPropSet/{token}` | POST | ✅ | ✅ | | 查询宠物检测开关 | `POST /gwp/v3/rtc/device/petDetectionSwitchStatus/{token}` | POST | ✅ | ✅ | | 设置宠物检测开关 | `POST /gwp/v3/rtc/device/petDetectionSwitchSetting/{token}` | POST | ✅ | ✅ | ## 核心功能 ### 1. 查询设备是否支持喂食（FeederAbility） **API:** `POST /gwp/v3/rtc/device/getability/{deviceToken}` **Name:** `FeederAbility` **响应参数：** | 字段 | 类型 | 说明 | |------|------|------| | `FeedBlockAlarm` | int | 堵粮报警设置开关，`1`=显示，`0`=不显示 | | `FoodShortageAlarm` | int | 余粮报警设置开关，`1`=显示，`0`=不显示 | | `BoxNums` | int | 食物餐盘个数。无该字段或值为 `0` 表示老设备，默认为 `1` | | `FeedSnap` | int | 是否支持喂食时抓图和喂食后延时抓图，`1`=支持，`0`=不支持 | ### 2. 一键喂食（Feed Control） **API:** `POST /gwp/v3/rtc/device/feeder/{deviceToken}` **请求参数：** | 参数 | 类型 | 必需 | 说明 | 取值 | |------|------|------|------|------| | `sn` | string | ✅ | 设备序列号 | - | | `props` | object | ✅ | 喂食参数 | - | | `props.feed` | int | ✅ | 喂食份数 | 正整数 | **请求示例：** ```json { "sn": "device-sn", "props": { "feed": 1 } } ``` ### 3. 定时喂食计划（Feed Schedule） **查询/设置 API:** `POST /gwp/v3/rtc/device/iotPropSet/{deviceToken}` **查询请求体：** ```json { "sn": "设备序列号", "props": ["feedPlan"] } ``` **设置请求体：** ```json { "sn": "设备序列号", "props": { "feedPlan": [ { "enable": true, "cron": "0 31 10 * * 1,2,3,4,5,6,0", "action": { "feed": 1 } } ] } } ``` **定时计划项（feedPlan）：** | 字段 | 类型 | 说明 | 取值 | |------|------|------|------| | `enable` | boolean | 定时器是否开启 | `true`=启用，`false`=禁用 | | `cron` | string | cron 时间表达式 | `0 {minute} {hour} * * {weekdays}` | | `action.feed` | int | 投食份数 | 正整数 | **操作方法（method）：** | 方法 | 说明 | |------|------| | `Add` | 增加自动喂食时间点 | | `Modify` | 修改已设定的自动喂食时间点 | | `Delete` | 删除已设定的自动喂食时间点 | | `Clear` | 删除全部自动喂食计划 | ### 4. 宠物检测开关（Pet Detection） **查询 API:** `POST /gwp/v3/rtc/device/petDetectionSwitchStatus/{deviceToken}` **设置 API:** `POST /gwp/v3/rtc/device/petDetectionSwitchSetting/{deviceToken}` **查询响应：** | 字段 | 类型 | 说明 | |------|------|------| | `Switch` | string | 开关状态：`ON`=开启，`OFF`=关闭 | | `Ret` | int | 设备响应状态码 | **设置请求体：** ```json { "Switch": "ON" } ``` ## 使用示例 ### 环境准备 ```bash # 设置环境变量（使用占位符，请替换为实际值） export JF_UUID="uuidxxxx" export JF_APP_KEY="appkeyxxxx" export JF_APP_SECRET="appsecretxxxx" export JF_MOVE_CARD="5" export JF_DEVICE_SN="snxxx1" export JF_DEVICE_TOKEN="NTQ0NzQ3YmE3MXwyYzFk..." export JF_ENDPOINT="api-cn.jftechws.com" ``` ### 1. 查询设备是否支持喂食 ```bash cd ~/.qoderwork/skills/jf-open-pro-feed-control/scripts python3 feed_control.py --action check-support ``` ### 2. 一键喂食 ```bash # 喂食一次（默认 1 份） python3 feed_control.py --action feed-once # 喂食 2 份 python3 feed_control.py --action feed-once --portion 2 ``` ### 3. 定时喂食计划 ```bash # 查询当前定时喂食计划 python3 feed_control.py --action get-schedule # 设置定时喂食计划（配置文件方式） python3 feed_control.py --action set-schedule --schedule-file schedule.json # 快速添加一条计划（每天 8:00 喂食 2 份） python3 feed_control.py --action add-schedule \ --hour 8 --minute 0 --portion 2 \ --repeat "Mon,Tue,Wed,Thu,Fri,Sat,Sun" ``` ### 4. 宠物检测 ```bash # 查询宠物检测开关状态 python3 feed_control.py --action get-pet-detect # 开启宠物检测 python3 feed_control.py --action set-pet-detect --enable true # 关闭宠物检测 python3 feed_control.py --action set-pet-detect --enable false ``` ## 状态码 ### 平台状态码 | code | 说明 | 处理建议 | |------|------|----------| | 2000 | 成功 | - | | 28007 | Header 参数错误 | 检查 uuid、appKey、timeMillis、signature | | 40103 | 无效 Token | deviceToken 过期，重新获取 | | 50000 | 服务器内部错误 | 联系杰峰技术支持 | ### 设备状态码（Ret） | Ret | 说明 | |-----|------| | 100 | 成功 | | 103 | 非法请求（设备不支持该功能） | | 106 | 用户名或密码错误 | ## 注意事项 1. **设备要求** - 仅智能宠物喂食器设备支持此功能 2. **deviceToken 有效期** - 24 小时，过期需重新获取 3. **设备在线要求** - 所有操作需要设备在线 4. **出粮份数** - 建议在设备支持的最小和最大份数范围内设置 5. **定时计划** - 最多支持 10 条定时计划 6. **重复周期** - 支持按星期重复，使用英文缩写：`Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun` ## 相关文件 | 文件 | 说明 | |------|------| | `SKILL.md` | 技能文档 | | `scripts/feed_control.py` | Python 执行脚本 | | `scripts/crypto.py` | 签名/时间戳加密工具（复用） | ## 参考文档 - [杰峰开放平台](https://docs.jftech.com)

path: scripts/crypto.py
content: #!/usr/bin/env python3
"""
杰峰 OpenAPI 加密工具

包含：
- 签名算法
- 时间戳生成
"""

import hashlib
import time


def get_counter() -> str:
    """
    获取计数器（7 位，不足补 0）
    
    Returns:
        7 位计数字符串
    """
    counter = 1
    if counter < 10:
        return '000000' + str(counter)
    elif counter < 100:
        return '00000' + str(counter)
    elif counter < 1000:
        return '0000' + str(counter)
    elif counter < 10000:
        return '000' + str(counter)
    elif counter < 100000:
        return '00' + str(counter)
    elif counter < 1000000:
        return '0' + str(counter)
    else:
        return str(counter)


def get_time_millis() -> str:
    """
    获取组合时间戳（counter 7 位 + timeMillis 13 位）
    
    Returns:
        20 位时间戳字符串
    """
    counter = get_counter()
    time_millis = str(int(time.time() * 1000))
    return counter + time_millis


def str_to_bytes(s: str) -> list:
    """
    字符串转字节数组（处理 UTF-8 多字节字符）
    
    Args:
        s: 输入字符串
        
    Returns:
        字节数组
    """
    bytes_arr = []
    for char in s:
        code = ord(char)
        if code >= 0x010000 and code <= 0x10FFFF:
            bytes_arr.append(((code >> 18) & 0x07) | 0xF0)
            bytes_arr.append(((code >> 12) & 0x3F) | 0x80)
            bytes_arr.append(((code >> 6) & 0x3F) | 0x80)
            bytes_arr.append((code & 0x3F) | 0x80)
        elif code >= 0x000800 and code <= 0x00FFFF:
            bytes_arr.append(((code >> 12) & 0x0F) | 0xE0)
            bytes_arr.append(((code >> 6) & 0x3F) | 0x80)
            bytes_arr.append((code & 0x3F) | 0x80)
        elif code >= 0x000080 and code <= 0x0007FF:
            bytes_arr.append(((code >> 6) & 0x1F) | 0xC0)
            bytes_arr.append((code & 0x3F) | 0x80)
        else:
            bytes_arr.append(code & 0xFF)
    return bytes_arr


def change(encrypt_str: str, move_card: int) -> list:
    """
    简单移位算法（严格按照 Java 版本实现）
    
    Args:
        encrypt_str: 输入字符串
        move_card: 移动卡标识
        
    Returns:
        移位后的字节数组
    """
    try:
        encrypt_byte = list(encrypt_str.encode('iso-8859-1'))
    except UnicodeEncodeError:
        encrypt_byte = str_to_bytes(encrypt_str)
    
    encrypt_length = len(encrypt_byte)
    
    for i in range(encrypt_length):
        if (i % move_card) > ((encrypt_length - i) % move_card):
            temp = encrypt_byte[i]
        else:
            temp = encrypt_byte[encrypt_length - (i + 1)]
        
        encrypt_byte[i] = encrypt_byte[encrypt_length - (i + 1)]
        encrypt_byte[encrypt_length - (i + 1)] = temp
    
    return encrypt_byte


def merge_byte(encrypt_byte: list, change_byte: list) -> list:
    """
    合并字节数组
    
    Args:
        encrypt_byte: 原字节数组
        change_byte: 移位后的字节数组
        
    Returns:
        合并后的字节数组
    """
    encrypt_length = len(encrypt_byte)
    encrypt_length2 = encrypt_length * 2
    temp = [0] * encrypt_length2
    
    for i in range(encrypt_length):
        temp[i] = encrypt_byte[i]
        temp[encrypt_length2 - 1 - i] = change_byte[i]
    
    return temp


def generate_signature(uuid: str, app_key: str, app_secret: str, 
                       time_millis: str, move_card: int) -> str:
    """
    生成杰峰 OpenAPI 签名
    
    算法步骤：
    1. 将 uuid, appKey, appSecret, timeMillis 依次拼接
    2. 获取拼接后字符串的二进制数组
    3. 二进制数组进行简单移位算法
    4. 原二进制数组与移位后的二进制数组合并
    5. 将合并的二进制数组 md5 加密一次生成密钥
    
    Args:
        uuid: 客户唯一标识
        app_key: 应用 key
        app_secret: 应用密钥
        time_millis: 时间戳
        move_card: 移动取模基数
        
    Returns:
        32 位十六进制签名字符串
    """
    encrypt_str = uuid + app_key + app_secret + time_millis
    
    try:
        encrypt_byte = list(encrypt_str.encode('iso-8859-1'))
    except UnicodeEncodeError:
        encrypt_byte = str_to_bytes(encrypt_str)
    
    change_byte = change(encrypt_str, move_card)
    merge = merge_byte(encrypt_byte, change_byte)
    signature = hashlib.md5(bytes(merge)).hexdigest()
    
    return signature


# 测试
if __name__ == "__main__":
    uuid = "uuidxxxx"
    app_key = "appkeyxxxx"
    app_secret = "90f8bc17be2a425db6068c749deexxxx"
    time_millis = "00000011645153792342"
    move_card = 2
    
    signature = generate_signature(uuid, app_key, app_secret, time_millis, move_card)
    
    print(f"Signature: {signature}")
    print(f"注意：签名算法需与杰峰服务器验证逻辑一致")


path: scripts/feed_control.py
content: #!/usr/bin/env python3
"""
杰峰智能宠物喂食器设备控制技能（开发版）

支持功能：
- 查询设备是否支持喂食
- 一键喂食
- 定时喂食计划查询和设置
- 宠物检测功能开关查询和设置
"""

import os
import sys
import json
import argparse
import requests
from typing import Optional, Dict, Any, List

# 导入加密工具（复用）
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
    from crypto import get_time_millis, generate_signature
except ImportError:
    print("[错误] 错误：找不到 crypto.py 模块")
    print("   请确保 scripts/crypto.py 存在")
    sys.exit(1)

# API 基础地址（可通过环境变量切换）
JF_ENDPOINT = os.getenv("JF_ENDPOINT", "api-cn.jftechws.com")
JF_BASE_URL = f"https://{JF_ENDPOINT}/gwp/v3"


def get_headers(uuid: str, app_key: str, app_secret: str, move_card: int) -> Dict[str, str]:
    """生成请求头（包含签名和时间戳）"""
    time_millis = get_time_millis()
    signature = generate_signature(uuid, app_key, app_secret, time_millis, move_card)
    
    return {
        "Content-Type": "application/json; charset=UTF-8",
        "uuid": uuid,
        "appKey": app_key,
        "timeMillis": time_millis,
        "signature": signature,
        "X-Request-Id": os.urandom(16).hex()
    }


def get_device_token(uuid: str, app_key: str, app_secret: str,
                     move_card: int, sns: List[str],
                     endpoint: str = "api-cn.jftechws.com",
                     access_token: str = "") -> List[Dict[str, str]]:
    """
    获取设备接口访问令牌（通用函数）

    API: POST /gwp/v3/rtc/device/token

    Args:
        uuid: 开放平台用户 uuid
        app_key: 开放平台应用 appKey
        app_secret: 开放平台应用密钥
        move_card: 移动卡标识
        sns: 设备序列号数组（最多 500 个）
        endpoint: API 接入地址
        access_token: 杰峰 AMS 用户系统 accessToken（可选）

    Returns:
        设备 token 列表，每项包含 sn 和 token
    """
    base_url = f"https://{endpoint}/gwp/v3"
    url = f"{base_url}/rtc/device/token"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {
        "sns": sns
    }
    if access_token:
        body["accessToken"] = access_token

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"获取设备 Token 失败：{result.get('msg', '未知错误')}")

    data = result.get("data", [])
    if not data:
        raise RuntimeError("设备不属于当前用户，或设备未绑定")

    return data


def check_feed_support(device_token: str, uuid: str, app_key: str,
                        app_secret: str, move_card: int) -> Dict[str, Any]:
    """
    查询设备是否支持喂食功能（FeederAbility）

    API: POST /gwp/v3/rtc/device/getability/{deviceToken}
    Name: FeederAbility
    """
    url = f"{JF_BASE_URL}/rtc/device/getability/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {"Name": "FeederAbility"}

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"查询设备能力失败：{result.get('msg', '未知错误')}")

    return result.get("data", {})


def feed_once(device_token: str, uuid: str, app_key: str,
              app_secret: str, move_card: int,
              device_sn: str, portion: int = 1) -> Dict[str, Any]:
    """
    一键喂食

    API: POST /gwp/v3/rtc/device/feeder/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/feeder/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {
        "sn": device_sn,
        "props": {
            "feed": portion
        }
    }

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"喂食指令发送失败：{result.get('msg', '未知错误')}")

    if result.get("data", {}).get("Ret") != 100:
        raise RuntimeError(f"设备返回错误：{result.get('data', {}).get('Ret')}")

    return result.get("data", {})


def get_feed_schedule(device_token: str, uuid: str, app_key: str,
                       app_secret: str, move_card: int,
                       device_sn: str) -> Dict[str, Any]:
    """
    获取定时喂食计划

    API: POST /gwp/v3/rtc/device/iotPropSet/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/iotPropSet/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {
        "sn": device_sn,
        "props": ["feedPlan"]
    }

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"获取定时喂食计划失败：{result.get('msg', '未知错误')}")

    return result.get("data", {})


def set_feed_schedule(device_token: str, uuid: str, app_key: str,
                       app_secret: str, move_card: int,
                       device_sn: str,
                       config: Dict[str, Any],
                       method: str = "") -> Dict[str, Any]:
    """
    设置定时喂食计划

    API: POST /gwp/v3/rtc/device/iotPropSet/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/iotPropSet/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {
        "sn": device_sn,
        "props": {
            "feedPlan": config
        }
    }
    if method:
        body["props"]["method"] = method

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"设置定时喂食计划失败：{result.get('msg', '未知错误')}")

    if result.get("data", {}).get("Ret") != 100:
        raise RuntimeError(f"设备返回错误：{result.get('data', {}).get('Ret')}")

    return result.get("data", {})


def get_pet_detect_config(device_token: str, uuid: str, app_key: str,
                           app_secret: str, move_card: int) -> Dict[str, Any]:
    """
    获取宠物检测开关状态

    API: POST /gwp/v3/rtc/device/petDetectionSwitchStatus/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/petDetectionSwitchStatus/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    response = requests.post(url, headers=headers, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"获取宠物检测状态失败：{result.get('msg', '未知错误')}")

    return result.get("data", {})


def set_pet_detect_config(device_token: str, uuid: str, app_key: str,
                           app_secret: str, move_card: int,
                           switch: str) -> Dict[str, Any]:
    """
    设置宠物检测开关状态

    API: POST /gwp/v3/rtc/device/petDetectionSwitchSetting/{deviceToken}
    """
    url = f"{JF_BASE_URL}/rtc/device/petDetectionSwitchSetting/{device_token}"
    headers = get_headers(uuid, app_key, app_secret, move_card)

    body = {"Switch": switch}

    response = requests.post(url, headers=headers, json=body, timeout=30)
    result = response.json()

    if result.get("code") != 2000:
        raise RuntimeError(f"设置宠物检测状态失败：{result.get('msg', '未知错误')}")

    if result.get("data", {}).get("Ret") != 100:
        raise RuntimeError(f"设备返回错误：{result.get('data', {}).get('Ret')}")

    return result.get("data", {})


def get_default_schedule_config() -> Dict[str, Any]:
    """获取默认定时喂食计划配置"""
    return [
        {
            "enable": True,
            "cron": "0 0 8 * * 1,2,3,4,5,6,0",
            "action": {
                "feed": 2
            }
        },
        {
            "enable": True,
            "cron": "0 0 18 * * 1,2,3,4,5,6,0",
            "action": {
                "feed": 2
            }
        }
    ]


# ============== 动作处理函数 ==============

def get_token_action(args: argparse.Namespace) -> int:
    """执行获取设备 Token 操作"""
    try:
        sns = args.device_sn.split(",") if args.device_sn else []
        if not sns:
            print("[错误] 错误：缺少 --device-sn 或 JF_DEVICE_SN 环境变量", file=sys.stderr)
            return 1

        print(f"正在获取 {len(sns)} 个设备的接口访问令牌...")

        tokens = get_device_token(
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            sns=sns,
            endpoint=JF_ENDPOINT
        )

        print("[OK] 获取成功")
        for item in tokens:
            print(f"   设备 SN: {item.get('sn')}")
            print(f"   DeviceToken: {item.get('token')}")
            print()

        # 如果只查询一个设备，提示可以直接使用
        if len(tokens) == 1:
            print("[提示] 可将上述 token 设置为环境变量后继续操作")
            print(f"   export JF_DEVICE_TOKEN=\"{tokens[0].get('token')}\"")

        return 0

    except RuntimeError as e:
        print(f"[错误] 错误：{e}", file=sys.stderr)
        return 1


def check_feed_support_action(args: argparse.Namespace) -> int:
    """执行查询设备是否支持喂食操作"""
    try:
        print(f"正在查询设备 {args.device_sn} 的喂食能力...")

        result = check_feed_support(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card
        )

        feeder = result.get("FeederAbility", {})
        if feeder:
            print("[OK] 查询成功")
            print(f"   堵粮报警：{'显示' if feeder.get('FeedBlockAlarm') else '不显示'}")
            print(f"   余粮报警：{'显示' if feeder.get('FoodShortageAlarm') else '不显示'}")

            box_nums = feeder.get("BoxNums")
            if box_nums is not None:
                print(f"   食物餐盘个数：{box_nums}")
            else:
                print("   食物餐盘个数：1（老设备默认值）")

            feed_snap = feeder.get("FeedSnap")
            if feed_snap is not None:
                print(f"   喂食抓图：{'支持' if feed_snap else '不支持'}")
            else:
                print("   喂食抓图：不支持")
        else:
            print("[警告] 设备未返回 FeederAbility 能力信息")

        print(f"   会话 ID: {result.get('SessionID')}")
        print(f"   Ret: {result.get('Ret')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def feed_once_action(args: argparse.Namespace) -> int:
    """执行一键喂食操作"""
    try:
        portion = args.portion
        print(f"正在向设备 {args.device_sn} 发送喂食指令（{portion} 份）...")
        
        result = feed_once(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            device_sn=args.device_sn,
            portion=portion
        )
        
        print(f"[OK] 喂食指令发送成功")
        print(f"   出粮份数：{portion}")
        print(f"   会话 ID: {result.get('SessionID')}")
        return 0
        
    except RuntimeError as e:
        print(f"[错误] 错误：{e}", file=sys.stderr)
        return 1


def get_feed_schedule_action(args: argparse.Namespace) -> int:
    """执行获取定时喂食计划操作"""
    try:
        print(f"正在获取设备 {args.device_sn} 的定时喂食计划...")

        config = get_feed_schedule(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            device_sn=args.device_sn
        )

        props = config.get("props", {})
        schedules = props.get("feedPlan", [])
        if schedules is not None:
            print("[OK] 获取成功")
            print(f"   计划数量：{len(schedules)}")
            print()
            for i, s in enumerate(schedules):
                status = "启用" if s.get("enable") else "禁用"
                cron = s.get("cron", "")
                feed = s.get("action", {}).get("feed", 1)
                print(f"   [{i + 1}] cron={cron}  {feed}份  [{status}]")
        else:
            print("[警告] 设备未返回定时喂食计划配置")

        print(f"   会话 ID: {config.get('SessionID')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def set_feed_schedule_action(args: argparse.Namespace) -> int:
    """执行设置定时喂食计划操作"""
    try:
        if args.schedule_file:
            with open(args.schedule_file, 'r', encoding='utf-8') as f:
                config = json.load(f)
            print(f"正在从文件 {args.schedule_file} 设置定时喂食计划...")
        else:
            print(f"正在设置设备 {args.device_sn} 的定时喂食计划...")
            config = get_default_schedule_config()

        result = set_feed_schedule(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            device_sn=args.device_sn,
            config=config
        )

        print("[OK] 设置成功")
        print(f"   会话 ID: {result.get('SessionID')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def add_schedule_action(args: argparse.Namespace) -> int:
    """执行添加单条定时计划操作"""
    try:
        print(f"正在添加定时计划到设备 {args.device_sn}...")

        # 星期映射: Mon=1, Tue=2, Wed=3, Thu=4, Fri=5, Sat=6, Sun=0
        weekday_map = {
            "Mon": "1", "Tue": "2", "Wed": "3", "Thu": "4",
            "Fri": "5", "Sat": "6", "Sun": "0"
        }
        repeat_days = args.repeat.split(",") if args.repeat else ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
        cron_weekdays = ",".join([weekday_map.get(d, d) for d in repeat_days])

        # cron 格式: 0 {minute} {hour} * * {weekdays}
        cron = f"0 {args.minute} {args.hour} * * {cron_weekdays}"

        # 先获取现有配置
        config_data = get_feed_schedule(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            device_sn=args.device_sn
        )

        props = config_data.get("props", {})
        schedules = props.get("feedPlan", []) if props else []
        if schedules is None:
            schedules = []

        # 构建新计划
        new_plan = {
            "enable": True,
            "cron": cron,
            "action": {
                "feed": args.portion
            }
        }

        schedules.append(new_plan)

        result = set_feed_schedule(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            device_sn=args.device_sn,
            config=schedules
        )

        print("[OK] 添加成功")
        print(f"   cron: {cron}")
        print(f"   份数: {args.portion}")
        print(f"   重复: {','.join(repeat_days)}")
        print(f"   会话 ID: {result.get('SessionID')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def get_pet_detect_action(args: argparse.Namespace) -> int:
    """执行获取宠物检测开关状态操作"""
    try:
        print(f"正在获取设备 {args.device_sn} 的宠物检测开关状态...")

        config = get_pet_detect_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card
        )

        switch = config.get("Switch", "")
        if switch:
            print("[OK] 获取成功")
            print(f"   宠物检测开关：{'开启' if switch == 'ON' else '关闭'}")
        else:
            print("[警告] 未返回宠物检测开关状态")

        print(f"   Ret: {config.get('Ret')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def set_pet_detect_action(args: argparse.Namespace) -> int:
    """执行设置宠物检测开关操作"""
    try:
        enable = args.enable.lower() == 'true'
        switch = "ON" if enable else "OFF"
        print(f"正在{'开启' if enable else '关闭'}设备 {args.device_sn} 的宠物检测...")

        result = set_pet_detect_config(
            device_token=args.device_token,
            uuid=args.uuid,
            app_key=args.app_key,
            app_secret=args.app_secret,
            move_card=args.move_card,
            switch=switch
        )

        print("[OK] 设置成功")
        print(f"   宠物检测：{'开启' if enable else '关闭'}")
        print(f"   Ret: {result.get('Ret')}")
        return 0

    except RuntimeError as e:
        print(f"[错误] {e}", file=sys.stderr)
        return 1


def main():
    """主函数"""
    parser = argparse.ArgumentParser(description="杰峰智能宠物喂食器设备控制技能")
    
    # 全局参数
    parser.add_argument("--action", required=True,
                        choices=[
                            "get-token", "check-support", "feed-once",
                            "get-schedule", "set-schedule", "add-schedule",
                            "get-pet-detect", "set-pet-detect"
                        ],
                        help="操作类型")
    parser.add_argument("--uuid", default=os.getenv("JF_UUID"),
                        help="开放平台用户 uuid")
    parser.add_argument("--app-key", default=os.getenv("JF_APP_KEY"),
                        help="应用 appKey")
    parser.add_argument("--app-secret", default=os.getenv("JF_APP_SECRET"),
                        help="应用密钥")
    parser.add_argument("--move-card", type=int, default=os.getenv("JF_MOVE_CARD", "2"),
                        help="移动卡标识")
    parser.add_argument("--device-sn", default=os.getenv("JF_DEVICE_SN"),
                        help="设备序列号")
    parser.add_argument("--device-token", default=os.getenv("JF_DEVICE_TOKEN"),
                        help="设备接口访问令牌")
    parser.add_argument("--channel", type=int, default=0,
                        help="通道号（默认 0）")
    
    # feed-once 参数
    parser.add_argument("--portion", type=int, default=1,
                        help="出粮份数（默认 1）")
    
    # set-schedule 参数
    parser.add_argument("--schedule-file", default=None,
                        help="定时计划配置文件路径")
    
    # add-schedule 参数
    parser.add_argument("--schedule-id", type=int, default=1,
                        help="计划编号（默认 1）")
    parser.add_argument("--hour", type=int, default=8,
                        help="小时（默认 8）")
    parser.add_argument("--minute", type=int, default=0,
                        help="分钟（默认 0）")
    parser.add_argument("--repeat", default="Mon,Tue,Wed,Thu,Fri,Sat,Sun",
                        help="重复周期（默认每天）")
    
    # set-pet-detect 参数
    parser.add_argument("--enable", default="true",
                        help="是否开启（true/false）")

    args = parser.parse_args()
    
    # 验证必需参数
    if not args.uuid:
        print("[错误] 错误：缺少 --uuid 或 JF_UUID 环境变量", file=sys.stderr)
        return 1
    if not args.app_key:
        print("[错误] 错误：缺少 --app-key 或 JF_APP_KEY 环境变量", file=sys.stderr)
        return 1
    if not args.app_secret:
        print("[错误] 错误：缺少 --app-secret 或 JF_APP_SECRET 环境变量", file=sys.stderr)
        return 1
    if args.action != "get-token" and not args.device_token:
        print("[错误] 错误：缺少 --device-token 或 JF_DEVICE_TOKEN 环境变量", file=sys.stderr)
        print("   提示：可先使用 --action get-token 获取 deviceToken", file=sys.stderr)
        return 1

    # 执行对应操作
    if args.action == "get-token":
        return get_token_action(args)
    elif args.action == "check-support":
        return check_feed_support_action(args)
    elif args.action == "feed-once":
        return feed_once_action(args)
    elif args.action == "get-schedule":
        return get_feed_schedule_action(args)
    elif args.action == "set-schedule":
        return set_feed_schedule_action(args)
    elif args.action == "add-schedule":
        return add_schedule_action(args)
    elif args.action == "get-pet-detect":
        return get_pet_detect_action(args)
    elif args.action == "set-pet-detect":
        return set_pet_detect_action(args)
    else:
        print(f"[错误] 未知操作：{args.action}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())


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