Skill: dolynkdeveloper/dahua-cloud-open-iot-basic-general-kit (version 1.0.0)

--- name: dahua-cloud-open-iot-basic-general-kit description: 大华云开放平台IoT设备管理统一客户端，支持摄像头、NVR、DVR等设备的完整生命周期管理。提供设备添加/删除/查询、国标GB28181设备接入、SD卡管理、WiFi配置、消息订阅回调、铃音管理、图片解密等43个API接口。适用于大华云IoT平台设备接入、监控系统集成、智能安防项目开发。使用场景：如何管理大华云设备、批量查询设备状态、获取设备在线状态、配置设备回调消息、国标设备接入平台、摄像头SD卡格式化、修改设备WiFi连接、设备图片解密、IoT设备运维管理。单文件Python实现，支持命令行CLI和SDK调用，自动Token刷新，零冗余配置。 metadata: {"openclaw":{"requires":{"env":["DAHUA_CLOUD_PRODUCT_ID","DAHUA_CLOUD_AK","DAHUA_CLOUD_SK"]},"primaryEnv":"DAHUA_CLOUD_SK","install":[{"id":"requests","kind":"python","package":"requests","label":"Install Python requests"},{"id":"pycryptodome","kind":"python","package":"pycryptodome","label":"Install Python pycryptodome"}]}} type: executable paths: - scripts/ dependencies: - requests - pycryptodome --- # 大华云开放平台 - 基础通用套件 ## 概述 本技能提供**大华云开放平台基础通用套件**的统一客户端实现，采用单文件设计： - ✅ **统一客户端** - 封装所有设备管理API - ✅ **零冗余配置** - 仅需 Cloud 凭证（ProductId、AK、SK）即可 - ✅ **单文件实现** - 易于维护和部署 - ✅ **环境变量支持** - 支持GUI/命令行配置 - ✅ **命令行工具** - 快速操作无需编写代码 - ✅ **Python SDK** - 可集成到其他应用 - ✅ **自动Token管理** - Token自动刷新，无需手动处理 --- ## 完整功能 共 **43 个接口** 全部支持，详见 [API_COVERAGE.md](./API_COVERAGE.md)。 ### 1. 设备管理 - 添加设备 `add_device()` / `add` - 校验设备密码 `verify_device_password()` / `verify` - 删除设备 `delete_device()` / `delete` - 查询设备列表 `get_device_list()` / `list` - 查询设备在线状态 `get_device_online()` / `online` - 批量查询设备详细信息 `list_device_details()` / `details` - 查询设备绑定状态 `check_device_bind()` / `bind` - 查询设备品类 `get_category()` / `category` - 获取设备通道信息 `get_device_channel_info()` / `channels` - 修改设备/通道名称 `modify_device_name()` - 修改设备密码 `modify_dev_code()` ### 2. 国标设备 - 获取国标设备注册信息 `get_sip_info()` / `sip-info` - 获取国标码列表 `list_gb_code()` / `gb-code` - 添加国标设备 `add_gb_device()` / `add-gb` - 修改国标设备信息 `modify_gb_device()` / `modify-gb` - 国标设备查询详细信息 `list_gb_device_details()` / `gb-details` ### 3. SD卡管理 - 查询SD卡状态 `get_sd_card_status()` - 查询SD卡容量 `get_sd_card_storage()` - 获取SD卡列表 `list_sd_card_storage()` - 格式化SD卡 `format_sd_card()` ### 4. 设备配置 - 获取设备使能开关状态 `get_ability_status()` / `ability` - 设置设备使能开关 `set_ability_status()` - 设备校时 `set_current_utc()` ### 5. 铃音管理 - 获取铃声列表 `list_custom_ring()` - 新增自定义铃声 `add_custom_ring()` - 删除自定义铃声 `delete_custom_ring()` - 设置铃声 `set_custom_ring()` ### 6. WiFi 管理 - 获取设备当前连接的热点信息 `current_device_wifi()` - 获取设备周边热点信息 `wifi_around()` - 修改设备连接热点 `control_device_wifi()` ### 7. 其他设备能力 - 获取 SIM 信号强度 `get_sim_signal_strength()` - 图片解密 `image_decrypt()` ### 8. 消息订阅 - 添加回调配置 `add_callback_config()` - 按设备ID列表订阅消息 `message_subscribe()` - 分页获取回调配置 `get_all_callback_config()` - 根据回调配置ID更新设备订阅 `update_subscribe_by_callback_config()` - 根据回调配置ID更新回调配置 `update_callback_config()` - 删除回调配置及相关订阅 `delete_callback_config()` - 根据回调配置ID搜索回调配置信息 `get_callback_config_info()` - 根据回调配置ID搜索已订阅的设备消息 `get_subscribe_info_by_callback_config()` - 根据设备ID查询消息订阅信息 `get_subscribe_info_by_device()` - 按设备类别获取支持的消息类型 `get_message_type_page()` --- ## 配置凭证 **需要设置 Cloud 凭证**（ProductId、AK、SK）！ ### 方式 1: 图形界面设置 (Windows GUI) **最适合初学者和不想用命令行的用户！** #### 步骤 1: 打开系统设置 1. 按下 `Win + R` 键，输入 `sysdm.cpl` 并回车 2. 或者右键点击"此电脑" → "属性" → "高级系统设置" #### 步骤 2: 进入环境变量设置 1. 在弹出的"系统属性"窗口中，切换到 **"高级"** 选项卡 2. 点击底部的 **"环境变量(N)..."** 按钮 #### 步骤 3: 创建用户环境变量 在 **"当前用户的变量(U)"** 区域（窗口上半部分），点击 **"新建(W)..."**： | 变量名 | 变量值 | 说明 | |--------|--------|------| | `DAHUA_CLOUD_PRODUCT_ID` | 你的 AppID | 应用 ID | | `DAHUA_CLOUD_AK` | 你的 AccessKey | 访问密钥 | | `DAHUA_CLOUD_SK` | 你的 SecretKey | 保密密钥 | **示例**: ``` 变量名：DAHUA_CLOUD_PRODUCT_ID 变量值：138XXXX731 变量名：DAHUA_CLOUD_AK 变量值：196XXXXXXXXXXXXX808 变量名：DAHUA_CLOUD_SK 变量值：naumXXXXXXXXXXXXXXXXyHxh ``` #### 步骤 4: 确认并保存 1. 每个变量都点击 **"确定"** 保存 2. 关闭所有窗口 3. **重要**: 重新打开命令行窗口才能生效 #### 快速截图指引 如需更详细的图文教程，请参考以下操作要点： - 确保在"用户变量"区域添加，而非"系统变量" - 变量名必须完全一致（区分大小写） - 变量值不要有多余空格 - 添加完成后需要重启终端 --- ### 方式 2: 命令行快速设置 **Windows PowerShell:** ```powershell [Environment]::SetEnvironmentVariable("DAHUA_CLOUD_PRODUCT_ID", "your_app_id", "User") [Environment]::SetEnvironmentVariable("DAHUA_CLOUD_AK", "your_access_key", "User") [Environment]::SetEnvironmentVariable("DAHUA_CLOUD_SK", "your_secret_key", "User") ``` **Linux/Mac:** ```bash export DAHUA_CLOUD_PRODUCT_ID='your_app_id' export DAHUA_CLOUD_AK='your_access_key' export DAHUA_CLOUD_SK='your_secret_key' ``` --- ### 方式 3: 命令行临时设置 **Windows PowerShell (临时):** ```powershell $env:DAHUA_CLOUD_PRODUCT_ID='your_app_id' $env:DAHUA_CLOUD_AK='your_access_key' $env:DAHUA_CLOUD_SK='your_secret_key' ``` **注意**: 仅在当前窗口有效，关闭后失效 --- ### 验证环境变量是否生效 **Linux/Mac:** ```bash echo $DAHUA_CLOUD_PRODUCT_ID echo $DAHUA_CLOUD_AK # 注意：不要打印 SK，避免泄露 ``` **Windows PowerShell:** ```powershell $env:DAHUA_CLOUD_PRODUCT_ID $env:DAHUA_CLOUD_AK ``` --- ## 快速开始 ### 基本使用 ```bash # 查看帮助 python scripts/dahua_iot_client.py --help # 添加设备 python scripts/dahua_iot_client.py add -d <设备序列号> -p admin123 -c IPC # 查询设备列表 python scripts/dahua_iot_client.py list -p 1 -s 10 # 查询设备在线状态 python scripts/dahua_iot_client.py online -d <设备序列号> # 校验设备密码（验证密码是否正确） python scripts/dahua_iot_client.py verify -d <设备序列号> -p admin123 -e aes256 # 删除设备 python scripts/dahua_iot_client.py delete -d <设备序列号> ``` ### 完整示例 ```bash # 完整设备接入流程（仅需 Cloud 凭证） cd scripts python dahua_iot_client.py add -d <设备序列号> -p admin123 -c IPC -e aes256 python dahua_iot_client.py online -d <设备序列号> python dahua_iot_client.py list -p 1 -s 20 -d <设备序列号前缀> ``` ### Python SDK 调用 > **导入说明**：在 `scripts/` 目录下运行，或将 `scripts/` 加入 `PYTHONPATH` 后使用。 ```python from dahua_iot_client import DahuaIoTClient, create_client_from_env # 方式1: 从环境变量创建客户端（verbose=True 打印API日志） client = create_client_from_env() # SDK 集成时建议关闭日志：create_client_from_env(verbose=False) # 方式2: 手动创建客户端 client = DahuaIoTClient( app_id='your_product_id', access_key='your_access_key', secret_key='your_secret_key' ) # 添加设备 result = client.add_device( device_id='<设备序列号>', device_password='admin123', category_code='IPC', encrypt_method='aes256' # 推荐 ) print(f"Add device result: {result}") # 校验设备密码（验证密码是否正确） verify_result = client.verify_device_password('<设备序列号>', 'admin123', encrypt_method='aes256') print(f"Password verify: {verify_result.get('success', False)}") # 查询设备在线状态 status = client.get_device_online('<设备序列号>') print(f"Online status: {status}") # 查询设备列表 devices = client.get_device_list(page_num=1, page_size=10) print(f"Device list: {devices}") # 获取SD卡状态 sd_status = client.get_sd_card_status('<设备序列号>') print(f"SD card status: {sd_status}") # 设置设备使能开关 result = client.set_ability_status( device_id='<设备序列号>', ability_type='localRecord', status='on', channel_id='0' ) ``` --- ## 工作流程 ``` ┌─────────────────────┐ │ 1. 初始化客户端 │ │ (配置凭证) │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 2. 自动获取 Token │ │ (AppAccessToken) │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 3. 调用业务API │ │ (设备管理/配置) │ └──────────┬──────────┘ ▼ ┌─────────────────────┐ │ 4. 返回结果 │ │ (JSON格式) │ └─────────────────────┘ ``` --- ## API 端点 | 模块 | 路径前缀 | 说明 | |------|----------|------| | 认证 | [REDACTED_SECRET] | 获取 AppAccessToken | | 设备管理 | `/open-api/api-iot/device/*` | addDevice、deleteDevice、verifyDevCode、getDeviceList、deviceOnline、listDeviceDetailsByIds、checkDeviceBindInfo、getCategory、modifyDeviceName、modifyDevCode、getDeviceChannelInfo | | 国标设备 | `/open-api/api-iot/device/*` | getSipInfo、listGbCode、addGbDevice、modifyGbDevice、deviceInfoDetailAll | | SD卡/铃音/WiFi | `/open-api/api-aiot/device/*` | getSDCardStatus、getSDCardStorage、listSDCardStorage、formatSDCard、listCustomRing、addCustomRing、deleteCustomRing、setCustomRing、wifiAround、currentDeviceWifi、controlDeviceWifi | | 设备配置 | `/open-api/api-aiot/device/*` | getAbilityStatus、setAbilityStatus、setCurrentUTC | | 其他 | [REDACTED_SECRET] | 获取 SIM 信号强度 | | 图片解密 | [REDACTED_SECRET] | 图片解密 | | 消息订阅 | `/open-api/api-message/*` | addCallbackConfig、getAllCallbackConfigId、messageSubscribeByDeviceIds、getMessageTypePage、getMessageSubscribeInfoByDeviceId、updateSubscribeByCallbackConfigId、updateByCallbackConfigId、deleteCallbackId、getInfoByCallbackConfigId、getSubscribeInfoByCallbackConfigId | 完整 API 参考见 [references/api_reference.md](./references/api_reference.md)。 --- ## 认证机制 ### 签名算法 **SHA512 HMAC 签名** - 两种签名方式： 1. **get_token_sign()** - 获取 Token 签名 ``` 签名因子: access_key + timestamp + nonce ``` 2. **business_api_sign()** - 业务 API 签名 ``` 签名因子: access_key + app_access_token + timestamp + nonce ``` ### 请求头结构 **认证请求头** (获取 Token): ```python { "Content-Type": "application/json", "AccessKey": access_key, "Timestamp": timestamp, "Nonce": nonce, "X-TraceId-Header": trace_id, "Sign": signature, # get_token_sign() 生成 "ProductId": app_id, "Version": "V1", "Sign-Type": "simple" } ``` **业务请求头** (所有设备管理API): ```python { "Content-Type": "application/json", "AccessKey": access_key, "Timestamp": timestamp, "Nonce": nonce, "X-TraceId-Header": trace_id, "Sign": signature, # business_api_sign() 生成 "Version": "V1", "Sign-Type": "simple", "AppAccessToken": app_token, "ProductId": app_id } ``` --- ## 代码架构 ``` ┌──────────────────────────────────────────────────────────────────┐ │ 常量配置层 (Constants) │ │ - API 端点、超时时间、环境变量名 │ └──────────────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────────────┐ │ 工具函数层 (Utilities) │ │ - get_token_sign() 获取 Token 签名 │ │ - business_api_sign() 业务 API 签名 │ │ - DeviceCodeEncryptor 设备密码加密（Base64/AES256） │ └──────────────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────────────┐ │ DahuaIoTClient 类 (43个接口) │ │ 设备: add/verify/delete/list/details/online/bind/category/channel │ │ 国标: get_sip_info/add_gb/list_gb_code/modify_gb/gb_details │ │ SD卡: get_status/storage/list/format 铃音: list/add/delete/set │ │ 配置: get/set_ability_status/set_current_utc │ │ WiFi: wifi_around/current/control SIM: get_sim_signal_strength │ │ 消息: callback/message_subscribe/update/delete/query │ │ 其他: image_decrypt 图片解密 │ └──────────────────────────────────────────────────────────────────┘ │ ┌──────────────────────────────────────────────────────────────────┐ │ 辅助函数层 │ │ - create_client_from_env() 从环境变量创建客户端 │ └──────────────────────────────────────────────────────────────────┘ ``` --- ## 设备密码加密 ### 方式一：Base64加密 ```python 格式："Dolynk_" + Base64(设备密码) 例如：若设备密码为 admin123 Base64(admin123) = YWRtaW4xMjM= 加密后字符串 = Dolynk_YWRtaW4xMjM= ``` ### 方式二：AES256加密（推荐） ```python 格式：Base64(Aes256(待加密内容, AesKey, IV初始向量)) 加密算法：Aes256/CBC/PKCS7 初始化向量IV：86E2DB6D77B5E9CD AesKey：Cut32(UpperCase(MD5-32位(UpperCase(sk)))) ``` **示例代码**: ```python from dahua_iot_client import DeviceCodeEncryptor encryptor = DeviceCodeEncryptor( secret_access_key="your_sk", device_id="your_device_id" ) # AES256加密（推荐） dev_code = encryptor.encrypt_aes256("admin123") # Base64加密 dev_code = encryptor.encrypt_base64("admin123") ``` --- ## 核心类和函数 ### `DahuaIoTClient` 类 统一客户端类，封装所有设备管理API。 #### 初始化参数 | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `app_id` | `str` | ✅ | 产品ID (ProductId) | | `access_key` | `str` | ✅ | 访问密钥 (AccessKey) | | `secret_key` | `str` | ✅ | 密钥 (SecretKey) | | `api_base_url` | `str` | ❌ | API基础地址（默认：https://open.cloud-dahua.com/） | | `verbose` | `bool` | ❌ | 是否打印API调用日志（默认：True，SDK集成时建议False） | #### 主要方法（43 个接口） | 分类 | 方法 | 说明 | CLI | |------|------|------|-----| | 设备 | `add_device()` | 添加设备 | `add` | | 设备 | `verify_device_password()` | 校验设备密码 | `verify` | | 设备 | `delete_device()` | 删除设备 | `delete` | | 设备 | `get_device_list()` | 查询设备列表 | `list` | | 设备 | `get_device_online()` | 查询设备在线状态 | `online` | | 设备 | `list_device_details()` | 批量查询设备详细信息 | `details` | | 设备 | `check_device_bind()` | 查询设备绑定状态 | `bind` | | 设备 | `get_category()` | 查询设备品类 | `category` | | 设备 | `get_device_channel_info()` | 获取设备通道信息 | `channels` | | 设备 | `modify_device_name()` | 修改设备/通道名称 | - | | 设备 | `modify_dev_code()` | 修改设备密码 | - | | 国标 | `get_sip_info()` | 获取国标设备注册信息 | `sip-info` | | 国标 | `list_gb_code()` | 获取国标码列表 | `gb-code` | | 国标 | `add_gb_device()` | 添加国标设备 | `add-gb` | | 国标 | `modify_gb_device()` | 修改国标设备信息 | `modify-gb` | | 国标 | `list_gb_device_details()` | 国标设备查询详细信息 | `gb-details` | | SD卡 | `get_sd_card_status()` | 获取SD卡状态 | - | | SD卡 | `get_sd_card_storage()` | 查询SD卡容量 | - | | SD卡 | `list_sd_card_storage()` | 获取SD卡列表 | - | | SD卡 | `format_sd_card()` | 格式化SD卡 | - | | 配置 | `get_ability_status()` | 获取设备使能状态 | `ability` | | 配置 | `set_ability_status()` | 设置设备使能状态 | - | | 配置 | `set_current_utc()` | 设备校时 | - | | 铃音 | `list_custom_ring()` | 获取铃声列表 | - | | 铃音 | `add_custom_ring()` | 新增自定义铃声 | - | | 铃音 | `delete_custom_ring()` | 删除自定义铃声 | - | | 铃音 | `set_custom_ring()` | 设置铃声 | - | | WiFi | `current_device_wifi()` | 获取设备当前连接热点 | - | | WiFi | `wifi_around()` | 获取设备周边热点 | - | | WiFi | `control_device_wifi()` | 修改设备连接热点 | - | | 其他 | `get_sim_signal_strength()` | 获取SIM信号强度 | - | | 其他 | `image_decrypt()` | 图片解密 | - | | 消息 | `add_callback_config()` | 添加回调配置 | - | | 消息 | `message_subscribe()` | 按设备ID订阅消息 | - | | 消息 | `get_all_callback_config()` | 分页获取回调配置 | - | | 消息 | `update_subscribe_by_callback_config()` | 根据回调配置更新订阅 | - | | 消息 | `update_callback_config()` | 更新回调配置 | - | | 消息 | `delete_callback_config()` | 删除回调配置 | - | | 消息 | `get_callback_config_info()` | 搜索回调配置信息 | - | | 消息 | `get_subscribe_info_by_callback_config()` | 搜索已订阅设备消息 | - | | 消息 | `get_subscribe_info_by_device()` | 按设备ID查询订阅信息 | - | | 消息 | `get_message_type_page()` | 按设备类别获取消息类型 | - | ### `DeviceCodeEncryptor` 类 设备密码加密工具类。 #### 方法 | 方法 | 说明 | |------|------| | `encrypt_base64(password)` | Base64加密 | | `encrypt_aes256(password)` | AES256加密（推荐） | --- ## 命令行参数 ### 通用参数 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--help` | `-h` | - | ❌ | 显示帮助信息 | ### 命令行子命令汇总 | 子命令 | 说明 | |--------|------| | `add` | 添加设备 | | `add-gb` | 添加国标设备 | | `delete` | 删除设备 | | `verify` | 校验设备密码 | | `list` | 查询设备列表 | | `details` | 查询设备详细信息 | | `online` | 查询设备在线状态 | | `bind` | 查询设备绑定状态 | | `category` | 查询设备品类 | | `channels` | 获取设备通道信息 | | `ability` | 获取设备使能状态 | | `gb-code` | 获取国标码列表 | | `sip-info` | 获取国标设备注册信息 | | `gb-details` | 查询国标设备详细信息 | | `modify-gb` | 修改国标设备信息 | | `encrypt` | 加密设备密码 | ### 子命令详情 #### `add` - 添加设备 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--device-id` | `-d` | `str` | ✅ | 设备序列号 | | `--password` | `-p` | `str` | ✅ | 设备密码 | | `--category` | `-c` | `str` | ❌ | 设备品类编码（默认：IPC） | | `--encrypt` | `-e` | `str` | ❌ | 加密方式（base64/aes256，默认：base64） | #### `delete` - 删除设备 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--device-id` | `-d` | `str` | ✅ | 设备序列号 | #### `list` - 查询设备列表 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--page` | `-p` | `int` | ❌ | 页码（默认：1） | | `--size` | `-s` | `int` | ❌ | 每页条数（默认：10） | | `--device-id` | `-d` | `str` | ❌ | 设备序列号（模糊查询） | #### `online` - 查询设备在线状态 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--device-id` | `-d` | `str` | ✅ | 设备序列号 | #### `verify` - 校验设备密码 验证指定设备的密码是否正确（调用 verifyDevCode 接口）。 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--device-id` | `-d` | `str` | ✅ | 设备序列号 | | `--password` | `-p` | `str` | ✅ | 设备密码（明文） | | `--encrypt` | `-e` | `str` | ❌ | 加密方式（base64/aes256，默认：base64） | ```bash # 校验设备密码 python scripts/dahua_iot_client.py verify -d <设备序列号> -p admin123 -e aes256 ``` #### `details` - 查询设备详细信息 | 参数 | 说明 | |------|------| | `device_ids` | 设备序列号（可多个，位置参数） | ```bash python scripts/dahua_iot_client.py details <设备序列号> <设备序列号2> ``` #### `category` - 查询设备品类 | 参数 | 简写 | 说明 | |------|------|------| | `--primary` | `-p` | 1级设备类别代码 | | `--second` | `-s` | 2级设备类别代码（如 IPC） | | `--model` | `-m` | 设备型号 | #### `bind` - 查询设备绑定状态 | 参数 | 简写 | 必填 | 说明 | |------|------|------|------| | `--device-id` | `-d` | ✅ | 设备序列号 | #### `channels` - 获取设备通道信息 | 参数 | 简写 | 必填 | 说明 | |------|------|------|------| | `--device-id` | `-d` | ✅ | 设备序列号 | #### `ability` - 获取设备使能状态 | 参数 | 简写 | 必填 | 说明 | |------|------|------|------| | `--device-id` | `-d` | ✅ | 设备序列号 | | `--ability-type` | `-a` | ✅ | 使能类型（如 localRecord、motionDetect、faceCapture） | | `--channel-id` | `-c` | ❌ | 通道号（通道级使能需传） | #### `add-gb` - 添加国标设备 | 参数 | 简写 | 必填 | 说明 | |------|------|------|------| | `--gb-code` | `-g` | ✅ | 国标码 | | `--password` | `-p` | ✅ | 设备注册密码 | | `--channels` | `-c` | ✅ | 通道总数 | | `--encrypt` | `-e` | ❌ | 加密方式（base64/aes256，默认 base64） | | `--manufacturer` | `-m` | ❌ | 厂商（Dahua、HIKVSION、UNKNOW） | | `--stream-model` | `-s` | ❌ | 拉流协议（TCP/UDP） | | `--device-class` | `-d` | ❌ | 设备类型（NVR/IPC） | #### `gb-code` - 获取国标码列表 | 参数 | 简写 | 说明 | |------|------|------| | `--count` | `-c` | 查询数量（1-10000，默认 10） | | `--prefix` | `-p` | 国标码前缀（长度必须 13 位） | #### `sip-info` - 获取国标设备注册信息 无需参数，返回 Sip 服务器 IP 和端口。 #### `gb-details` - 查询国标设备详细信息 | 参数 | 说明 | |------|------| | `device_ids` | 设备序列号（可多个，位置参数） | #### `modify-gb` - 修改国标设备信息 | 参数 | 简写 | 必填 | 说明 | |------|------|------|------| | `--device-id` | `-d` | ✅ | 设备序列号 | | `--password` | `-p` | ❌ | 新设备密码 | | `--encrypt` | `-e` | ❌ | 密码加密方式（base64/aes256，默认：base64） | | `--stream-model` | `-s` | ❌ | 拉流协议（TCP/UDP） | #### `encrypt` - 加密设备密码 | 参数 | 简写 | 类型 | 必填 | 说明 | |------|------|------|------|------| | `--password` | `-p` | `str` | ✅ | 设备密码 | | `--secret-key` | `-s` | `str` | ✅ | SecretKey | | `--device-id` | `-d` | `str` | ❌ | 设备序列号 | | `--method` | `-m` | `str` | ❌ | 加密方式（base64/aes256，默认：base64） | --- ## 响应格式 所有 API 响应遵循统一格式： ```python { "code": "200", # 响应状态码 "success": true, # 响应状态 "data": {}, # 响应数据 "msg": "操作成功" # 响应消息 } ``` ### add_device 响应示例 ```python { "success": True, "code": "200", "msg": "操作成功", "data": { "deviceId": "<设备序列号>", "deviceName": "", "channelNum": 1, "status": 1 } } ``` ### get_device_online 响应示例 ```python { "success": True, "code": "200", "msg": "操作成功", "data": { "deviceId": "<设备序列号>", "online": True } } ``` **错误码说明**: - `200`: 成功 - 其他: 失败，查看 `msg` 字段了解错误原因 --- ## 可配置常量 | 常量名 | 默认值 | 说明 | |--------|--------|------| | `DEFAULT_API_BASE_URL` | `https://open.cloud-dahua.com/` | API 基础地址 | | `TOKEN_EXPIRY_SECONDS` | `604800` | Token 有效期(秒) - 7天 | | `TIMEOUT_AUTH` | `60` | 认证超时(秒) | | `TIMEOUT_DEVICE` | `60` | 设备管理超时(秒) | | `TIMEOUT_CONFIG` | `60` | 配置超时(秒) | | `TIMEOUT_SD_CARD` | `60` | SD卡管理超时(秒) | | `TIMEOUT_FORMAT_SD` | `120` | SD卡格式化超时(秒) | --- ## 依赖要求 - Python 3.8+ - requests>=2.31.0 - pycryptodome>=3.15.0 安装依赖: ```bash pip install requests pycryptodome ``` --- ## 使用场景 ### 1️ 设备接入场景 ```bash # 添加设备 python scripts/dahua_iot_client.py add -d <设备序列号> -p admin123 -c IPC # 校验设备密码（验证密码是否正确） python scripts/dahua_iot_client.py verify -d <设备序列号> -p admin123 -e aes256 # 查询设备在线状态 python scripts/dahua_iot_client.py online -d <设备序列号> # 查询设备列表/详细信息 python scripts/dahua_iot_client.py list -d <设备序列号前缀> python scripts/dahua_iot_client.py details <设备序列号> ``` ### 2️ 设备管理场景 ```python # Python SDK client = create_client_from_env() # 获取设备列表 devices = client.get_device_list(page_num=1, page_size=100) # 批量查询设备详情 device_ids = ['<设备序列号1>', '<设备序列号2>'] details = client.list_device_details(device_ids) # 删除设备 client.delete_device('<设备序列号>') ``` ### 3️ SD卡管理场景 ```python # 查询SD卡状态 status = client.get_sd_card_status('<设备序列号>') # 查询SD卡容量 storage = client.get_sd_card_storage('<设备序列号>') # 格式化SD卡 result = client.format_sd_card('<设备序列号>') ``` ### 4️ 设备配置场景 ```python # 修改设备名称 client.modify_device_name('<设备序列号>', 'My Camera') # 启用本地录像 client.set_ability_status( device_id='<设备序列号>', ability_type='localRecord', status='on', channel_id='0' ) ``` ### 5️ 消息订阅场景 ```python # 添加回调配置（返回的 data 为回调配置ID） callback = client.add_callback_config( callback_url='https://your-server.com/callback', is_push=True ) callback_config_id = callback['data'] # API 返回的 data 即为 callbackConfigId # 订阅设备消息 client.message_subscribe( device_ids=['<设备序列号>'], message_type_codes=['online', 'offline', 'videoMotion'], category_code='IPC', callback_config_id=callback_config_id ) ``` ### 6️ 国标设备场景 ```bash # 获取国标码 python scripts/dahua_iot_client.py gb-code -c 5 # 获取 Sip 注册信息 python scripts/dahua_iot_client.py sip-info # 添加国标设备 python scripts/dahua_iot_client.py add-gb -g 34020000001320000001 -p admin123 -c 4 -e aes256 ``` ```python # Python SDK gb_codes = client.list_gb_code(count=10) sip_info = client.get_sip_info() result = client.add_gb_device(gb_code='34020000001320000001', device_password='admin123', channel_number=4) ``` ### 7️ 铃音 / WiFi / 图片解密 ```python # 铃音管理 rings = client.list_custom_ring(device_id, relate_type='device') # relate_type: device/channel client.add_custom_ring(device_id, name='my_ring', url='https://...', ring_type='custom') # WiFi 管理 wifi_info = client.current_device_wifi(device_id) around = client.wifi_around(device_id) client.control_device_wifi(device_id, ssid='MyWiFi', [REDACTED_SECRET]') # 图片解密（image_url 为待解密的图片地址） decrypted = client.image_decrypt(device_id, image_url='https://...') ``` --- ## 安全提示 ⚠️ **不要将真实的 Cloud 凭证提交到 Git!** 本项目包含 `.gitignore` 文件，会自动忽略敏感配置文件。建议： - 使用环境变量存储凭证 - 定期轮换密钥 - 限制应用权限范围 - Windows 用户建议使用 GUI 方式设置（更安全） --- ## 目录结构 ``` dahua-cloud-open-iot-basic-general-kit/ ├── README.md # 快速说明 ├── QUICKSTART.md # 5分钟快速入门 ├── SKILL.md # 本文件（完整指南） ├── FAQ.md # 常见问题解答 ├── .gitignore # Git 忽略规则 ├── references/ │ └── api_reference.md # 完整API参考文档（43个接口） └── scripts/ ├── dahua_iot_client.py # 统一客户端（推荐使用） ├── sign_helper.py # 签名工具（旧版，保留兼容） ├── device_code_encrypt.py # 密码加密工具（旧版，保留兼容） └── requirements.txt # Python依赖 ``` --- ## 核心优势 ✅ **极简配置** - 仅需要 3 个环境变量 (ProductId, AK, SK) ✅ **单文件实现** - 所有功能集中在一个文件，易于维护 ✅ **统一客户端** - 封装所有API，无需关心签名细节 ✅ **自动Token管理** - Token自动刷新，无需手动处理 ✅ **命令行支持** - 快速操作无需编写代码 ✅ **Python SDK** - 可轻松集成到其他应用 ✅ **跨平台支持** - Windows/Linux/Mac 完美运行 ✅ **GUI 友好** - Windows 图形界面设置环境变量 ✅ **轻量依赖** - 仅依赖 requests 和 pycryptodome ✅ **安全可靠** - 标准 API 认证，无密码泄露风险 --- ## 迁移指南 ### 从旧版迁移 如果你之前使用 `sign_helper.py` 和 `device_code_encrypt.py`，可以这样迁移： **旧版代码**: ```python from sign_helper import DahuaAPISigner from device_code_encrypt import DeviceCodeEncryptor signer = DahuaAPISigner(access_key, secret_key, product_id) encryptor = DeviceCodeEncryptor(secret_key, device_id) # 手动管理Token auth_headers = signer.sign_for_auth_api() # ... 手动获取Token ... # 手动构造请求 business_headers = signer.sign_for_business_api(app_token) # ... 手动调用API ... ``` **新版代码**: ```python from dahua_iot_client import create_client_from_env # 自动从环境变量读取配置 client = create_client_from_env() # 自动管理Token，直接调用API result = client.add_device( device_id='<设备序列号>', device_password='admin123' ) ``` --- ## 常见问题 ### Q1: Token过期怎么办？ **解决方案**: 客户端会自动管理Token，在过期前自动刷新，无需手动处理。 ### Q2: 签名验证失败？ **检查清单**: - [ ] AccessKey、SecretKey、ProductID是否正确 - [ ] 环境变量是否正确设置 - [ ] 是否重新打开了命令行窗口（环境变量需重启终端生效） ### Q3: 设备添加失败？ **可能原因**: - 设备密码加密方式不正确（推荐使用AES256） - 设备序列号已存在 - 设备品类编码不正确 - 设备未初始化或未设置密码 ### Q4: 如何判断设备是否支持某个功能？ **方法**: 调用 `list_device_details()` 接口，查看返回的 `deviceAbility` 或 `channelAbility` 字段，检查是否包含相应的能力集标识。 ### Q5: 如何验证设备密码是否正确？ **方法**: 使用 `verify` 子命令或 `verify_device_password()` 方法调用 verifyDevCode 接口： ```bash # 命令行 python scripts/dahua_iot_client.py verify -d <设备ID> -p <密码> -e aes256 ``` ```python # Python SDK result = client.verify_device_password(device_id, password, encrypt_method='aes256') # result['success'] 为 True 表示密码正确 ``` 更多常见问题请参阅 [FAQ.md](./FAQ.md)。 --- ## License MIT License

path: API_COVERAGE.md
content: # dahua-cloud-open-iot-basic-general-kit 接口支持检查

## 支持情况汇总

| 序号 | 接口名称 | 状态 | 说明 |
|------|----------|------|------|
| 1 | 获取国标设备注册信息 | ✅ 支持 | `get_sip_info()` / `sip-info` |
| 2 | 添加设备 | ✅ 支持 | `add_device()` / `add` |
| 3 | 批量查询设备详细信息 | ✅ 支持 | `list_device_details()` / `details` |
| 4 | 修改设备或通道名称 | ✅ 支持 | `modify_device_name()` |
| 5 | 获取国标码列表 | ✅ 支持 | `list_gb_code()` / `gb-code` |
| 6 | 修改国标设备信息 | ✅ 支持 | `modify_gb_device()` / `modify-gb` |
| 7 | 获取AppAccessToken | ✅ 支持 | 内部 `_get_app_access_token()`，自动调用 |
| 8 | 添加国标设备 | ✅ 支持 | `add_gb_device()` / `add-gb` |
| 9 | 删除设备 | ✅ 支持 | `delete_device()` / `delete` |
| 10 | 获取设备使能开关状态 | ✅ 支持 | `get_ability_status()` / `ability` |
| 11 | 国标设备查询详细信息 | ✅ 支持 | `list_gb_device_details()` / `gb-details` |
| 12 | 修改设备密码 | ✅ 支持 | `modify_dev_code()` |
| 13 | 获取设备在线状态 | ✅ 支持 | `get_device_online()` / `online` |
| 14 | 查询设备绑定状态 | ✅ 支持 | `check_device_bind()` / `bind` |
| 15 | 设备校时 | ✅ 支持 | `set_current_utc()` |
| 16 | 查询设备品类 | ✅ 支持 | `get_category()` / `category` |
| 17 | 设置设备使能开关 | ✅ 支持 | `set_ability_status()` |
| 18 | 验证设备密码 | ✅ 支持 | `verify_device_password()` / `verify` |
| 19 | 查询设备SD卡容量 | ✅ 支持 | `get_sd_card_storage()` |
| 20 | 获取铃声列表 | ✅ 支持 | `list_custom_ring()` |
| 21 | 新增自定义铃声 | ✅ 支持 | `add_custom_ring()` |
| 22 | 获取设备SD卡状态 | ✅ 支持 | `get_sd_card_status()` |
| 23 | 获取设备SD卡列表 | ✅ 支持 | `list_sd_card_storage()` |
| 24 | 删除自定义铃声 | ✅ 支持 | `delete_custom_ring()` |
| 25 | 设置铃声 | ✅ 支持 | `set_custom_ring()` |
| 26 | 格式化设备SD卡 | ✅ 支持 | `format_sd_card()` |
| 27 | 获取设备当前连接的热点信息 | ✅ 支持 | `current_device_wifi()` |
| 28 | 修改设备连接热点 | ✅ 支持 | `control_device_wifi()` |
| 29 | 获取设备周边热点信息 | ✅ 支持 | `wifi_around()` |
| 30 | 获取设备通道信息 | ✅ 支持 | `get_device_channel_info()` / `channels` |
| 31 | 获取sim信号强度 | ✅ 支持 | `get_sim_signal_strength()` |
| 32 | 查询设备列表 | ✅ 支持 | `get_device_list()` / `list` |
| 33 | 图片解密 | ✅ 支持 | `image_decrypt()` |
| 34 | 根据回调配置ID更新设备订阅消息 | ✅ 支持 | `update_subscribe_by_callback_config()` |
| 35 | 添加回调配置 | ✅ 支持 | `add_callback_config()` |
| 36 | 按设备类别获取支持的消息类型 | ✅ 支持 | `get_message_type_page()` |
| 37 | 按设备ID列表订阅消息 | ✅ 支持 | `message_subscribe()` |
| 38 | 按回调配置ID更新回调配置 | ✅ 支持 | `update_callback_config()` |
| 39 | 删除回调配置及相关订阅消息 | ✅ 支持 | `delete_callback_config()` |
| 40 | 按回调配置ID搜索回调配置信息 | ✅ 支持 | `get_callback_config_info()` |
| 41 | 根据回调配置ID搜索已订阅的设备消息 | ✅ 支持 | `get_subscribe_info_by_callback_config()` |
| 42 | 根据设备id查询消息订阅信息 | ✅ 支持 | `get_subscribe_info_by_device()` |
| 43 | 分页获取回调配置ID和回调配置地址 | ✅ 支持 | `get_all_callback_config()` |

## 统计

- **已实现**: 43 个
- **未实现**: 0 个
- **支持率**: 100%

## 新增方法一览

所有 43 个接口均已实现为 `DahuaIoTClient` 方法，可通过 Python SDK 调用。


path: FAQ.md
content: # 常见问题 FAQ

## Q1: 设置了环境变量但仍然报错？

**检查步骤**:

1. 是否**重新打开了命令行窗口**？
   - 新打开的窗口才会加载新的环境变量
   - 已打开的窗口不会自动更新

2. 环境变量是否设置正确？
   ```powershell
   # Windows 验证
   $env:DAHUA_CLOUD_PRODUCT_ID
   $env:DAHUA_CLOUD_AK
   # 注意：不要打印 SK，避免泄露
   ```

3. 是否在正确的区域添加？
   - 确保在"用户变量"而非"系统变量"（或统一在同一区域）
   - 变量名必须完全一致（区分大小写）

---

## Q2: 图形界面找不到"环境变量"在哪里？

**快速入口方法**:

### 方法 A: Win + R 快捷键
```
1. 按下 Win + R
2. 输入 sysdm.cpl
3. 点确定
```

### 方法 B: 右键"此电脑"
```
1. 右键"此电脑"
2. 选择"属性"
3. 点击"高级系统设置"
4. 点"环境变量"
```

### 方法 C: 搜索
```
1. 按 Win 键
2. 输入"环境变量"
3. 选择"编辑系统环境变量"
```

---

## Q3: 如何删除已有的环境变量？

**Windows GUI**:
1. 打开"环境变量"设置
2. 在"用户变量"列表中找到对应的变量
3. 选中后点击 **"删除(D)..."**
4. 确认删除

---

## Q4: 可以测试环境变量是否已设置吗？

**Windows**:
```powershell
# 测试单个变量
Get-ChildItem Env: | Where-Object {$_.Name -like "*DAHUA*"}

# 查看所有 Dahua 相关变量
$env:DAHUA_CLOUD_PRODUCT_ID
$env:DAHUA_CLOUD_AK
# 注意：不要打印 SK
```

**Linux/Mac**:
```bash
# 列出所有包含 DAHUA 的环境变量
env | grep DAHUA
```

---

## Q5: 设备添加失败怎么办？

**可能原因**:
- 设备密码加密方式不正确（推荐使用 AES256）
- 设备序列号已存在
- 设备品类编码不正确（如 IPC、NVR）
- 设备未初始化或未设置密码
- 设备未连接网络

**解决方法**:
1. 使用 `encrypt` 子命令验证加密结果：`python dahua_iot_client.py encrypt -p admin123 -s <SK> -d <设备ID> -m aes256`
2. 检查设备是否已在大华云平台注册
3. 确认设备品类编码与设备类型匹配
4. 查看 API 返回的 `msg` 字段了解具体错误

---

## Q6: Token 过期怎么办？

**解决方案**: 客户端会自动管理 Token，在过期前自动刷新，无需手动处理。Token 有效期为 7 天。

---

## Q7: 签名验证失败？

**检查清单**:
- [ ] AccessKey、SecretKey、ProductID 是否正确
- [ ] 环境变量是否正确设置（区分大小写）
- [ ] 是否重新打开了命令行窗口（环境变量需重启终端生效）
- [ ] 系统时间是否同步（签名依赖时间戳）

---

## Q8: 设备离线怎么办？

**可能原因**:
- 设备电源未开启
- 设备网络连接异常
- 设备未正确注册到大华云平台

**解决方法**:
1. 检查设备电源和网络连接
2. 登录大华云平台确认设备状态
3. 调用 `get_device_online` 确认返回的在线状态

---

## Q9: SD 卡格式化失败？

**可能原因**:
- 设备不支持远程格式化
- SD 卡处于读写状态
- 设备离线

**解决方法**:
1. 确认设备支持 `formatSDCard` 能力
2. 调用 `list_device_details` 查看 `deviceAbility` 是否包含 SD 卡相关能力
3. 确保设备在线后再执行

---

## Q10: 如何获取 Cloud 凭证？

1. 登录 [大华云开发者平台](https://open.cloud-dahua.com/)
2. 注册并创建应用
3. 在应用详情页获取：
   - Product ID (AppID)
   - Access Key (AK)
   - Secret Key (SK)

---

## Q11: 支持哪些操作系统？

- ✅ Windows 10/11
- ✅ Linux (Ubuntu/CentOS 等)
- ✅ macOS

---

## Q12: 如何从旧版 sign_helper 迁移？

参考 [SKILL.md](./SKILL.md) 中的「迁移指南」章节，使用 `create_client_from_env()` 替代手动管理 Token 和签名。

---

## Q13: 作为 SDK 集成时如何关闭日志输出？

创建客户端时设置 `verbose=False`：

```python
client = create_client_from_env(verbose=False)
# 或
client = DahuaIoTClient(app_id='...', access_key='...', secret_key='...', verbose=False)
```

---

## Q14: Python 导入 dahua_iot_client 失败？

确保满足以下之一：
1. 在 `scripts/` 目录下运行 Python
2. 将 `scripts/` 加入 `PYTHONPATH`：`export PYTHONPATH="${PYTHONPATH}:$(pwd)/scripts"`
3. 使用 `sys.path.insert(0, 'scripts')` 后再 `import dahua_iot_client`

---

**还有其他问题？** 请查阅 [SKILL.md](./SKILL.md) 获取更详细的技术文档。


path: QUICKSTART.md
content: # 快速入门指南

## 5分钟快速上手

### 第一步：安装依赖

```bash
cd scripts
pip install -r requirements.txt
```

### 第二步：配置凭证

**Windows PowerShell (推荐)**:
```powershell
# 临时设置（当前窗口有效）
$env:DAHUA_CLOUD_PRODUCT_ID='your_product_id'
$env:DAHUA_CLOUD_AK='your_access_key'
$env:DAHUA_CLOUD_SK='your_secret_key'

# 或永久设置（所有窗口有效）
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_PRODUCT_ID", "your_product_id", "User")
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_AK", "your_access_key", "User")
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_SK", "your_secret_key", "User")
```

**Linux/Mac**:
```bash
export DAHUA_CLOUD_PRODUCT_ID='your_product_id'
export DAHUA_CLOUD_AK='your_access_key'
export DAHUA_CLOUD_SK='your_secret_key'
```

### 第三步：使用客户端

#### 方式一：命令行工具

```bash
# 查看帮助
python dahua_iot_client.py --help

# 添加设备
python dahua_iot_client.py add -d <设备序列号> -p admin123

# 查询设备列表
python dahua_iot_client.py list

# 查询设备在线状态
python dahua_iot_client.py online -d <设备序列号>

# 删除设备
python dahua_iot_client.py delete -d <设备序列号>
```

#### 方式二：Python代码

> 在 `scripts/` 目录下运行，或先将 `scripts/` 加入 PYTHONPATH。

```python
from dahua_iot_client import create_client_from_env

# 创建客户端（自动从环境变量读取凭证）
client = create_client_from_env()
# SDK 集成时可关闭日志：create_client_from_env(verbose=False)

# 添加设备
result = client.add_device(
    device_id='<设备序列号>',
    device_password='admin123',
    category_code='IPC'
)
print(f"添加设备: {result}")

# 查询设备在线状态
status = client.get_device_online('<设备序列号>')
print(f"在线状态: {status}")

# 查询设备列表
devices = client.get_device_list(page_num=1, page_size=10)
print(f"设备列表: {devices}")

# 获取SD卡状态
sd_status = client.get_sd_card_status('<设备序列号>')
print(f"SD卡状态: {sd_status}")
```

## 常用命令速查

### 设备管理

```bash
# 添加设备（AES256加密）
python dahua_iot_client.py add -d <设备序列号> -p <设备密码> -c IPC

# 查询设备列表
python dahua_iot_client.py list -p 1 -s 10

# 查询设备在线状态
python dahua_iot_client.py online -d <设备序列号>

# 删除设备
python dahua_iot_client.py delete -d <设备序列号>
```

### 密码加密

```bash
# 加密设备密码
python dahua_iot_client.py encrypt -p <密码> -s <SecretKey> -m aes256
```

## 常见问题

### Q: 环境变量设置后不生效？

**A**: Windows下需要重新打开命令行窗口，或使用：
```powershell
# 刷新环境变量
$env:DAHUA_CLOUD_PRODUCT_ID = [Environment]::GetEnvironmentVariable("DAHUA_CLOUD_PRODUCT_ID", "User")
$env:DAHUA_CLOUD_AK = [Environment]::GetEnvironmentVariable("DAHUA_CLOUD_AK", "User")
$env:DAHUA_CLOUD_SK = [Environment]::GetEnvironmentVariable("DAHUA_CLOUD_SK", "User")
```

### Q: Token过期怎么办？

**A**: 客户端会自动管理Token，无需手动处理。

### Q: 如何查看API调用详情？

**A**: 客户端会自动打印请求和响应信息：
```
[API] POST /open-api/api-iot/device/addDevice
[DATA] {"deviceId": "<设备序列号>", ...}
[RESP] {"success": true, ...}
```

## 下一步

- 📖 阅读完整文档：[SKILL.md](./SKILL.md)
- 🔍 查看API参考：[references/api_reference.md](./references/api_reference.md)
- ❓ 常见问题：[FAQ.md](./FAQ.md)

## 需要帮助？

如有问题，请查看：
1. [FAQ.md](./FAQ.md) - 常见问题解答
2. [SKILL.md](./SKILL.md) - 完整使用指南
3. [references/api_reference.md](./references/api_reference.md) - 详细 API 说明


path: README.md
content: # 大华云开放平台 - 基础通用套件

大华云开放平台基础通用套件客户端，提供设备管理、SD卡、配置、消息订阅等通用接口。

## 快速开始

### 1. 安装依赖

```bash
cd scripts
pip install -r requirements.txt
```

### 2. 配置凭证

**Windows PowerShell (临时)**:
```powershell
$env:DAHUA_CLOUD_PRODUCT_ID='your_product_id'
$env:DAHUA_CLOUD_AK='your_access_key'
$env:DAHUA_CLOUD_SK='your_secret_key'
```

**Windows (永久)**:
```powershell
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_PRODUCT_ID", "your_product_id", "User")
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_AK", "your_access_key", "User")
[Environment]::SetEnvironmentVariable("DAHUA_CLOUD_SK", "your_secret_key", "User")
```

**Linux/Mac**:
```bash
export DAHUA_CLOUD_PRODUCT_ID='your_product_id'
export DAHUA_CLOUD_AK='your_access_key'
export DAHUA_CLOUD_SK='your_secret_key'
```

### 3. 使用命令行

```bash
# 查看帮助
python dahua_iot_client.py --help

# 添加设备
python dahua_iot_client.py add -d <设备序列号> -p admin123

# 查询设备列表
python dahua_iot_client.py list

# 查询设备在线状态
python dahua_iot_client.py online -d <设备序列号>

# 删除设备
python dahua_iot_client.py delete -d <设备序列号>
```

### 4. 使用Python SDK

在 `scripts/` 目录下运行：

```python
from dahua_iot_client import create_client_from_env

# 从环境变量创建客户端
client = create_client_from_env()

# 添加设备
result = client.add_device(
    device_id='<设备序列号>',
    device_password='admin123',
    category_code='IPC'
)

# 查询设备在线状态
status = client.get_device_online('<设备序列号>')

# 查询设备列表
devices = client.get_device_list(page_num=1, page_size=10)
```

## 主要功能

- ✅ 设备管理（添加/删除/查询）
- ✅ SD卡管理（状态查询/容量查询/格式化）
- ✅ 设备配置（名称修改/使能开关）
- ✅ 消息订阅（回调配置/消息订阅）
- ✅ 自动Token管理
- ✅ 命令行工具
- ✅ Python SDK

## 详细文档

- [QUICKSTART.md](./QUICKSTART.md) - 5 分钟快速入门
- [SKILL.md](./SKILL.md) - 完整使用指南
- [FAQ.md](./FAQ.md) - 常见问题解答
- [references/api_reference.md](./references/api_reference.md) - API 参考文档

## 核心优势

- **单文件实现** - 所有功能集中在 `dahua_iot_client.py`
- **自动 Token 管理** - 无需手动处理 Token 刷新
- **统一客户端** - 封装所有 API 调用
- **环境变量配置** - 支持 GUI 和命令行配置
- **跨平台支持** - Windows/Linux/Mac 完美运行

## 安全提示

⚠️ 不要将真实的 Cloud 凭证提交到 Git！建议使用环境变量存储凭证。

## License

MIT License


path: references/api_reference.md
content: # 设备基础服务API接口参考文档

## API调用基础信息

### 接入地址
- 中国数据中心: `https://open.cloud-dahua.com`

### 请求方式
- 所有API接口使用POST方法
- Content-Type: `application/json`

### 请求头参数

| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| Content-Type | String | 是 | application/json |
| Accept-Language | String | 是 | 国际化语言类型，如：zh-CN |
| Version | String | 是 | 协议版本号，如：v1 |
| AccessKey | String | 是 | 产品对应的AccessKey |
| AppAccessToken | String | 否 | 授权API非必填；业务API必填 |
| UserAccessToken | String | 否 | 业务应用开放相关API选填 |
| Timestamp | Long | 是 | 13位标准时间戳，请求时间与服务端相差不能超过5分钟 |
| Nonce | String | 是 | 随机数，用于安全验证 |
| Sign | String | 是 | 采用指定签名算法计算出的签名 |
| X-TraceId-Header | String | 是 | 链路跟踪ID，如UUID生成随机字符串 |
| ProductID | Integer | 是 | 产品ID |
| Sign-Type | String | 否 | 值为simple表示使用简化签名模式 |

### 简化签名模式

#### 授权API签名算法
```
strAuthFactor = AccessKey + Timestamp + Nonce;
sign = HMAC-SHA512(strAuthFactor, secretAccessKey).toUpperCase();
```

#### 业务API签名算法
```
strAuthFactor = AccessKey + AppAccessToken + Timestamp + Nonce;
sign = HMAC-SHA512(strAuthFactor, secretAccessKey).toUpperCase();
```

## 鉴权认证

### 获取AppAccessToken

**接口说明**: 开发者可以通过调用当前接口来获取AppAccessToken，只有经过AppAccessToken身份验证后才能调用常用API。

**URL**: `/open-api/api-base/auth/getAppAccessToken`

**请求参数**: 无请求体，通过请求头传递参数

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.appAccessToken | string | 是 | 鉴权令牌 |
| data.validitySeconds | string | 是 | 令牌的有效时间（单位：秒） |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
请求头:
{
  "AccessKey": "1680145896563675136",
  "Timestamp": "1689838378183",
  "Nonce": "lgfm8vnhu3qxu3csv8d0fuuj7elsftq0",
  "Sign": "85119125974213185EB25E1198350B8EDDF74A54EA08E092E472675E36FB353B953257F62A71A9EA8058C6882BDFBB80466CF770D3EC3E734A2C0BC817BB4E49",
  "Version": "v1",
  "X-TraceId-Header": "6tefz7oa3uvncpb2edui5svs29zia538",
  "ProductId": "856047897"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "appAccessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
    "validitySeconds": 604800
  },
  "msg": "Operation is successful."
}
```

## 设备管理

### 添加设备

**接口说明**: 在添加设备之前，需要初始化设备并设置设备的密码。

**设备密码加密方式**:

**方式一：Base64加密**
```
格式："Dolynk_" + "Base64(设备web密码)"
例如：若设备密码为 admin123
Base64(admin123) = YWRtaW4xMjM=
加密后字符串为 = Dolynk_YWRtaW4xMjM=
```

**方式二：AES256加密**
```
格式：Base64(Aes256(待加密内容, AesKey, IV初始向量))
加密算法：Aes256/CBC/PKCS7
初始化向量IV：86E2DB6D77B5E9CD
AesKey：Cut32(UpperCase(MD5-32位(UpperCase(sk))))
```

**URL**: `/open-api/api-iot/device/addDevice`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| devCode | string | 是 | 设备密码（加密后） |
| categoryCode | string | 否 | 设备品类 |
| devAccount | string | 否 | 目前支持admin或非admin添加 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "8E0******7D",
  "categoryCode": "IPC",
  "devCode": "1H14BbU*******9b+MJQ=="
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": null,
  "msg": "Operation is successful."
}
```

### 添加国标设备

**接口说明**: 国标设备接入说明

**URL**: `/open-api/api-iot/device/addGbDevice`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| devCode | string | 是 | 注册密码（AES256加密） |
| gbCode | string | 是 | 国标码 |
| manufacturer | string | 否 | 厂商：Dahua、HIKVSION、UNKNOW |
| channelNumber | integer | 是 | 通道总数 |
| gbStreamModel | string | 否 | 拉流协议：TCP或UDP，默认UDP |
| deviceClass | string | 否 | 设备类型：NVR（默认）、IPC |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.deviceId | string | 是 | 设备序列号 |
| data.manufacturer | string | 是 | 厂商 |
| data.state | string | 是 | 设备状态 |
| data.deviceNo | string | 是 | 设备编号 |
| data.protocolType | string | 是 | 协议类型 |
| data.name | string | 是 | 设备名称 |
| data.deviceClass | string | 是 | 设备类型 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "devCode": "7FXsUuW2uvyJONMreznmTA==",
  "gbCode": "00043363807410000003",
  "manufacturer": "Dahua",
  "channelNumber": 10,
  "gbStreamModel": "TCP",
  "deviceClass": "IPC"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "deviceId": "QO1Y7OAGMDL5V6RSUUXDXDTV0001",
    "manufacturer": "Dahua",
    "state": "offline",
    "deviceNo": "00043363807410000003",
    "protocolType": "GB28181",
    "name": "",
    "deviceClass": "IPC"
  },
  "msg": "Operation is successful."
}
```

### 获取国标码列表

**接口说明**: 该接口用于获取指定数量的国标码。

**URL**: `/open-api/api-iot/device/listGbCode`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| count | number | 是 | 单次查询数量（最大10000，最小1） |
| prefix | number | 否 | 自定义国标码前缀（长度必须13位） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.maxGbCode | string | 是 | 最大国标编码 |
| data.minGbCode | string | 是 | 最小国标编码 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "count": 5,
  "prefix": "00161****0395"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "minGbCode": "00057981****0000039",
    "maxGbCode": "00057981****0000044"
  },
  "msg": "操作成功"
}
```

### 获取国标设备注册信息

**接口说明**: 该接口用于获取Sip服务器IP和端口号。

**URL**: `/open-api/api-iot/device/getSipInfo`

**请求参数**: 无请求体

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.sipServerIp | string | 是 | Sip服务器Ip和端口 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "sipServerIp": "121.41.9.73:5060"
  },
  "msg": "操作成功"
}
```

### 删除设备

**接口说明**: 删除已添加的设备。

**URL**: `/open-api/api-iot/device/deleteDevice`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "8D******C0"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": null,
  "msg": "Operation is successful."
}
```

## SD卡管理

### 格式化设备SD卡

**接口说明**: 设备需要有LocalStorage能力集。

**URL**: `/open-api/api-aiot/device/formatSDCard`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| index | number | 否 | SD卡编号（单卡设备无需传；多卡设备不传默认格式化第一张） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.result | string | 是 | 设备响应结果：start-recover、no-sdcard、in-recover、sdcard-error、formated |

**请求示例**:
```json
{
  "deviceId": "8H0C6F5YAG05880",
  "index": 1
}
```

**响应示例**:
```json
{
  "msg": "操作成功。",
  "success": true,
  "code": "200",
  "data": {
    "result": "start-recover"
  }
}
```

### 获取设备SD卡状态

**接口说明**: 设备需要有LocalStorage能力集。

**URL**: `/open-api/api-aiot/device/getSDCardStatus`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| index | number | 否 | SD卡编号（单卡设备无需传；多卡设备不传默认查询第一张） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.status | string | 是 | 存储卡状态：empty、normal、abnormal、recovering、needinit等 |

**请求示例**:
```json
{
  "deviceId": "8HXXXXXXXX80",
  "index": 1
}
```

**响应示例**:
```json
{
  "msg": "操作成功。",
  "success": true,
  "code": "200",
  "data": {
    "status": "normal"
  }
}
```

### 查询设备SD卡容量

**接口说明**: 设备需要有LocalStorage能力集。

**URL**: `/open-api/api-aiot/device/getSDCardStorage`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| index | string | 否 | SD卡编号（单卡设备无需传；多卡设备不传默认查询第一张） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.totalBytes | number | 是 | 总容量（单位：Byte） |
| data.usedBytes | number | 是 | 已使用容量（单位：Byte） |

**请求示例**:
```json
{
  "deviceId": "8HXXXXXXXXX880",
  "index": "1"
}
```

**响应示例**:
```json
{
  "msg": "操作成功。",
  "success": true,
  "code": "200",
  "data": {
    "totalBytes": 7816249344,
    "usedBytes": 6856704000
  }
}
```

### 获取设备SD卡列表

**接口说明**: 设备需要有MultipleLocalStorage能力集。

**URL**: `/open-api/api-aiot/device/listSDCardStorage`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.storageList | object[] | 是 | SD卡的信息列表 |
| data.storageList[].name | string | 是 | SD卡名称 |
| data.storageList[].index | string | 是 | SD卡编号（从1开始） |

**请求示例**:
```json
{
  "deviceId": "8HXXXXXXX80"
}
```

**响应示例**:
```json
{
  "msg": "success。",
  "success": true,
  "code": "200",
  "data": {
    "storageList": [
      {
        "index": "1",
        "name": "/dev/mmc0"
      },
      {
        "index": "2",
        "name": "/dev/mmc1"
      }
    ]
  }
}
```

## 设备查询

### 查询设备品类

**接口说明**: 区分设备类别以获得不同类别支持的不同设备类型。

**URL**: `/open-api/api-iot/device/getCategory`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| secondCategoryCode | string | 否 | 2级设备类别代码 |
| primaryCategoryCode | string | 否 | 1级设备类别代码 |
| deviceModel | string | 否 | 设备型号（仅支持每次传一个） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.categoryList | object[] | 否 | 类别列表 |
| data.categoryList[].primaryCategoryName | string | 是 | 1级设备品类名称 |
| data.categoryList[].primaryCategoryCode | string | 是 | 1级设备品类编号 |
| data.categoryList[].secondCategoryName | string | 是 | 2级设备品类名称 |
| data.categoryList[].secondCategoryCode | string | 是 | 2级设备品类编码 |
| data.categoryList[].deviceModel | string[] | 否 | 设备型号列表 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "secondCategoryCode": "IPC"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "categoryList": [
      {
        "primaryCategoryCode": "Camera",
        "primaryCategoryName": "视频通用",
        "secondCategoryName": "双目IPC",
        "secondCategoryCode": "BinocularIPC",
        "deviceModel": [
          "DH-H6B-E2"
        ]
      }
    ]
  },
  "msg": "Operation is successful."
}
```

### 查询设备绑定状态

**接口说明**: 查询单个设备是否绑定到某个帐户。

**URL**: `/open-api/api-iot/device/checkDeviceBindInfo`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.bindStatus | string | 是 | 绑定状态 |
| data.deviceExist | string | 是 | 平台上是否存在设备 |
| data.status | string | 是 | 设备在线状态 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "bindStatus": "bind",
    "deviceExist": null,
    "status": null
  },
  "msg": "Operation is successful."
}
```

### 获取设备在线状态

**接口说明**: 获取设备的在离线状态。

**URL**: `/open-api/api-iot/device/deviceOnline`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.deviceId | string | 否 | 设备序列号 |
| data.onLine | string | 否 | 1：设备在线，0：设备离线 |
| data.channels | object[] | 否 | 通道列表 |
| data.channels[].channelId | string | 否 | 设备通道号 |
| data.channels[].onLine | string | 否 | 通道在线状态 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "channels": [
      {
        "channelId": "0",
        "onLine": "1"
      }
    ],
    "deviceId": "<设备序列号>",
    "onLine": "1"
  },
  "msg": "Operation is successful."
}
```

### 获取设备通道信息

**接口说明**: 查询设备通道下挂载设备的序列号、设备大类、设备型号信息。

**URL**: `/open-api/api-aiot/device/getDeviceChannelInfo`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.channels | object[] | 是 | 通道列表 |
| data.channels[].channelId | string | 否 | 设备通道号 |
| data.channels[].deviceInfo | object | 否 | 通道设备信息 |
| data.channels[].deviceInfo.deviceClass | string | 否 | 通道设备大类 |
| data.channels[].deviceInfo.deviceType | string | 否 | 通道设备型号 |
| data.channels[].deviceInfo.deviceId | string | 否 | 通道下设备序列号 |

**请求示例**:
```json
{
  "deviceId": "AA03012XXXXXX"
}
```

**响应示例**:
```json
{
  "msg": "Operation is successful.",
  "success": true,
  "code": "200",
  "data": {
    "channels": [
      {
        "channelId": 0,
        "deviceInfo": {
          "deviceClass": "IPC",
          "deviceType": "DH-IPC-xxxxx",
          "deviceId": "9C063xxxx"
        }
      }
    ]
  }
}
```

### 批量查询设备详细信息

**接口说明**: 批量查询设备详细信息。

**URL**: `/open-api/api-iot/device/listDeviceDetailsByIds`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceList | object[] | 是 | 设备序列号列表（最大size=100） |
| deviceList[].deviceId | string | 是 | 设备序列号 |
| deviceList[].channelId | string[] | 否 | 通道列表（最大size=100） |

**响应参数**: 复杂的嵌套结构，包含设备详细信息、通道信息等。

**请求示例**:
```json
{
  "deviceList": [
    {
      "deviceId": "<设备序列号>",
      "channelId": ["0"]
    }
  ]
}
```

### 查询设备列表

**接口说明**: 查询设备和设备下通道的信息。

**URL**: `/open-api/api-iot/device/getDeviceList`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| pageNum | number | 是 | 页码（最小为1） |
| pageSize | string | 是 | 每页条数（最小为1，最大100） |
| deviceId | string | 否 | 设备序列号（支持模糊查询） |
| primaryCategoryCodeList | string[] | 否 | 设备品类列表 |

**响应参数**: 复杂的嵌套结构，包含分页信息和设备列表。

**请求示例**:
```json
{
  "pageSize": "1",
  "pageNum": "10"
}
```

### 国标设备查询详细信息

**接口说明**: 该接口用于批量查询国标设备的详细信息。

**URL**: `/open-api/api-iot/device/deviceInfoDetailAll`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceIds | string[] | 是 | 设备序列号列表 |

**响应参数**: 复杂的嵌套结构，包含设备详细信息和通道信息。

**请求示例**:
```json
{
  "deviceIds": ["QO1Y7OAGMDL5V6RSUUXDXDTV0001"]
}
```

## 设备配置

### 基础配置

#### 修改国标设备信息

**接口说明**: 该接口用于修改国标设备信息。

**URL**: `/open-api/api-iot/device/modifyGbDevice`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| devCode | string | 否 | 设备密码（AES256加密） |
| gbStreamModel | string | 否 | 国标设备拉流协议：TCP或UDP |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| msg | string | 是 | 响应消息 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "deviceId": "QO1Y7OAGMDL5V6RSUUXDXDTV0001",
  "devCode": "7FXsUuW2uvyJONMreznmTA=="
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {},
  "msg": "Operation is successful."
}
```

#### 验证设备密码

**接口说明**: 适用于需要验证密码的场景。

**URL**: `/open-api/api-iot/device/verifyDevCode`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| devCode | string | 是 | 设备密码（AES-256加密） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>",
  "devCode": "xxxx"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {},
  "msg": "Operation is successful."
}
```

#### 修改设备或通道名称

**接口说明**: 更改设备或通道名称并将其直接下发到设备。

**URL**: `/open-api/api-iot/device/modifyDeviceName`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| channelId | string | 否 | 设备通道号 |
| name | string | 是 | 设备或者通道名称 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 否 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>",
  "name": "developerTest01"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {},
  "msg": "Operation is successful."
}
```

#### 修改设备密码

**接口说明**: 修改设备密码。

**URL**: `/open-api/api-iot/device/modifyDevCode`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| oldDevCode | string | 是 | 设备老密码（AES-256加密） |
| newDevCode | string | 是 | 设备新密码（AES-256加密） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>",
  "oldDevCode": "1H14BbU+W7AD5vm09b+MJQ==",
  "newDevCode": "1H14BbU+W7AD5vm09b+MJQ=="
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {},
  "msg": "Operation is successful."
}
```

### 设备使能

#### 获取设备使能开关状态

**接口说明**: 获取设备使能开关状态。

**URL**: `/open-api/api-iot/device/getAbilityStatus`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| abilityType | string | 是 | 设备使能类型 |
| channelId | string | 否 | 通道号（设备级使能无需传；通道级使能需要传） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.status | string | 是 | 开关状态：on、off |
| data.abilityType | string | 是 | 启用的设备功能的类型 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>",
  "abilityType": "localRecord",
  "channelId": "0"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "abilityType": "localRecord",
    "status": "off"
  },
  "msg": "Operation is successful."
}
```

#### 设置设备使能开关

**接口说明**: 远程配置使能开关的状态以启用或禁用设备功能。

**URL**: `/open-api/api-iot/device/setAbilityStatus`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| abilityType | string | 是 | 要启用的能力的类型 |
| status | string | 是 | 生效与否：on：已启用，off：禁用 |
| channelId | string | 否 | 通道号（设备级使能无需传；通道级使能需要传） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "<设备序列号>",
  "abilityType": "localRecord",
  "status": "on",
  "channelId": "1"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {},
  "msg": "Operation is successful."
}
```

### 设备时间配置

#### 设备校时

**接口说明**: 用于修改设备时间。

**URL**: `/open-api/api-aiot/device/setCurrentUTC`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| tolerance | number | 是 | 容差（单位：秒） |
| utc | number | 是 | UTC时间戳（不带时区夏令时偏差0时区时间戳截止到秒） |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 返回错误码 |
| success | boolean | 是 | 是否成功 |
| msg | string | 是 | 返回消息 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "deviceId": "AB0233BPAJXXXX",
  "tolerance": 5,
  "utc": 1718696202
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "msg": "操作成功。",
  "data": {}
}
```

### 铃音配置

#### 新增自定义铃声

**接口说明**: 设备需要有CustomRing的能力集。

**URL**: `/open-api/api-aiot/device/addCustomRing`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| channelId | string | 否 | 通道序列号 |
| name | string | 是 | 铃声名称（长度不超过32） |
| type | string | 是 | 铃声文件格式类型：wav、pcm、aac |
| url | string | 是 | 铃声文件地址（长度不超过512） |
| relateType | string | 否 | 铃声关联类型：device、reply、local、quickReply、chime |
| language | string | 否 | 文件语言：English、Russian、Turkey等 |
| version | string | 否 | 文件版本 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "name": "q666677778888",
  "channelId": "1",
  "relateType": "device",
  "type": "aac",
  "deviceId": "AEXXXXXXXX193",
  "url": "http://s3.ap-southeast-...",
  "language": "English",
  "version": "V1.000.0000000.0.R.240521"
}
```

**响应示例**:
```json
{
  "code": "200",
  "msg": "success",
  "success": true,
  "data": {}
}
```

#### 删除自定义铃声

**接口说明**: 能力集：CustomRing。

**URL**: `/open-api/api-aiot/device/deleteCustomRing`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| index | string | 是 | 铃声序列号 |
| relateType | string | 是 | 铃声关联类型：device、reply、local、quickReply、chime |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态码 |
| success | boolean | 是 | 响应状态 |
| msg | string | 是 | 响应消息 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "deviceId": "AE0XXXXXXX193",
  "index": "3",
  "relateType": "device"
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "msg": "success",
  "data": {}
}
```

#### 获取铃声列表

**接口说明**: 设备需要有CustomRing能力集。

**URL**: `/open-api/api-aiot/device/listCustomRing`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| relateType | string | 是 | 铃声关联类型：device、reply、local、quickReply、chime |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态码 |
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.list | object[] | 是 | 铃声列表 |
| data.list[].name | string | 是 | 铃声名称 |
| data.list[].index | number | 是 | 铃声序列号 |
| data.list[].ringMode | string | 是 | 铃声模式：default、custom |
| data.list[].state | string | 是 | 铃声状态：play、download |
| data.list[].type | string | 是 | 铃声类型：wav、pcm、aac |
| data.ringIndex | number | 是 | 用户配置的铃声索引 |

**请求示例**:
```json
{
  "deviceId": "AEXXXXXXXX193",
  "relateType": "device"
}
```

**响应示例**:
```json
{
  "code": "200",
  "msg": "success",
  "success": true,
  "data": {
    "list": [
      {
        "index": "0",
        "name": "You are under surveillance",
        "ringMode": "default",
        "state": "play",
        "type": "wav"
      }
    ],
    "ringIndex": 1
  }
}
```

#### 设置铃声

**接口说明**: 设备需要有CustomRing能力集。

**URL**: `/open-api/api-aiot/device/setCustomRing`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| channelId | string | 否 | 通道序列号 |
| index | string | 是 | 铃声序列号 |
| relateType | string | 是 | 铃声关联类型：device、reply、local、quickReply、chime |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态码 |
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "deviceId": "AEXXXXXXX193",
  "channelId": "1",
  "index": "4",
  "relateType": "device"
}
```

**响应示例**:
```json
{
  "code": "200",
  "msg": "success",
  "success": true,
  "data": {}
}
```

### 网络配置

#### 获取设备周边热点信息

**接口说明**: 要求设备具有WIFI能力集。

**URL**: `/open-api/api-aiot/device/wifiAround`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应信息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.wLan | object[] | 是 | 热点信息集合 |
| data.wLan[].linkStatus | number | 是 | 连接状态：0未连接、1连接中、2已连接 |
| data.wLan[].intensity | number | 是 | 信号强度（1~5） |
| data.wLan[].auth | string | 是 | 认证模式：OPEN、WEP、WPA/WPA2、WPA/WPA2 PSK |
| data.wLan[].bssid | string | 是 | BSSID（MAC地址） |
| data.wLan[].ssid | string | 是 | 热点ID |
| data.enable | boolean | 是 | 是否开启了wifi |

**请求示例**:
```json
{
  "deviceId": "abc123"
}
```

#### 获取设备当前连接的热点信息

**接口说明**: 要求设备具有WIFI能力集。

**URL**: `/open-api/api-aiot/device/currentDeviceWifi`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应信息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.intensity | string | 是 | 信号强度（1~5） |
| data.ssid | string | 是 | 当前连接的热点名称 |
| data.linkEnable | boolean | 是 | 是否连接了wifi |

**请求示例**:
```json
{
  "deviceId": "abc123"
}
```

#### 修改设备连接热点

**接口说明**: 要求设备具有WIFI能力集。

**URL**: `/open-api/api-aiot/device/controlDeviceWifi`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| ssid | string | 是 | 需要连接的SSID |
| bssid | string | 否 | 需要连接的BSSID |
| password | string | 否 | wifi密码（base64加密） |
| linkEnable | boolean | 否 | 连接或断开：true连接，false断开 |
| channelSn | string | 否 | 配件序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应信息 |
| code | string | 是 | 响应状态码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |

**请求示例**:
```json
{
  "deviceId": "123456",
  "password": "123456",
  "ssid": "123456",
  "bssid": "12:34:56:78:90:12",
  "password": [REDACTED_SECRET],
  "linkEnable": true
}
```

#### 查询单SIM卡设备的SIM、IMEI信息

**接口说明**: 需要设备具有 SMSCSS 或 SSIM 能力集。

**URL**: （未提供）

#### 获取sim信号强度

**接口说明**: 需要设备具有 SMSCSS 或 SSIM 能力集。

**URL**: `/open-api/api-aiot/device/getSimSignalStrength`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| msg | string | 是 | 响应消息 |
| success | boolean | 是 | 响应状态 |
| code | string | 是 | 响应状态码 |
| data | object | 是 | 响应数据 |
| data.intensity | number | 是 | 信号强度（单位：dbm） |
| data.sigStrength | string | 是 | 信号强度等级：0-5（5最强） |

**请求示例**:
```json
{
  "deviceId": "abc123"
}
```

**响应示例**:
```json
{
  "msg": "操作成功。",
  "success": true,
  "code": "200",
  "data": {
    "intensity": 94,
    "sigStrength": "5"
  }
}
```

## 消息订阅

### 图片解密

**接口说明**: 解密加密的报警信息缩略图。

**URL**: `/open-api/api-decrypto/image/decrypto`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |
| imageUrl | string | 是 | 需要解密的加密图像地址（有效期一天） |
| devCode | string | 否 | 设备密码（AES256加密，密钥使用SK） |
| isTcmDevice | boolean | 否 | 设备能力集包含TCM或ESB5时需要密码校验 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.url | string | 否 | 图像URL（有效期一天） |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "AC02295PHA00343",
  "imageUrl": "https://lc-jn-hs-online-paas-private-picture...",
  "isTcmDevice": false
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": {
    "url": "http://test-developer-bkt-oss-bucket..."
  },
  "msg": "Operation is successful."
}
```

### 按照设备订阅消息

#### 分页获取回调配置ID和回调配置地址

**接口说明**: 分页获取回调配置ID和回调配置地址。

**URL**: `/open-api/api-message/getAllCallbackConfigId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 默认值 | 描述 |
|---------|---------|---------|--------|------|
| pageSize | number | 否 | 20 | 每页记录数 |
| pageNum | number | 否 | 1 | 页码 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.currentPage | number | 是 | 当前页 |
| data.totalPage | number | 是 | 总页数 |
| data.pageSize | number | 是 | 每页数量 |
| data.totalRows | number | 是 | 总数量 |
| data.pageData | object[] | 是 | 当前页数据 |
| data.pageData[].callbackConfigId | number | 否 | 回调配置ID |
| data.pageData[].callbackUrl | string | 否 | 回调配置地址 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "pageSize": 10,
  "pageNum": 1
}
```

#### 根据设备ID查询消息订阅信息

**接口说明**: 根据设备ID查询消息订阅信息。

**URL**: `/open-api/api-message/getMessageSubscribeInfoByDeviceId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceId | string | 是 | 设备序列号 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object[] | 是 | 响应数据 |
| data[].callbackUrl | string | 否 | 回调配置地址 |
| data[].isPush | boolean | 否 | 是否开启推送：false关闭，true开启 |
| data[].messageTypeCodes | string[] | 否 | 消息类型列表 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceId": "8E034DAPAJ00XXX"
}
```

#### 按设备ID列表订阅消息

**接口说明**: 相同类型消息的第二个配置不会覆盖原始配置。列表中的设备必须属于同一产品且为相同品类编码。

**URL**: `/open-api/api-message/messageSubscribeByDeviceIds`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| deviceIds | string[] | 是 | 设备序列号列表（最大数量为50） |
| messageTypeCodes | string[] | 是 | 消息类型 |
| categoryCode | string | 是 | 设备品类编码 |
| callbackConfigId | number | 是 | 回调配置ID |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "deviceIds": ["AF741****S41001"],
  "messageTypeCodes": ["TiltAlarm", "online"],
  "callbackConfigId": 1781218942140944393,
  "categoryCode": "ARC"
}
```

#### 按回调配置ID更新回调配置

**接口说明**: 更新回调配置。

**URL**: `/open-api/api-message/updateByCallbackConfigId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| callbackConfigId | number | 是 | 回调配置ID |
| callbackUrl | string | 否 | 回调地址URL |
| isPush | boolean | 否 | 是否开启消息推送：false关闭，true开启 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackConfigId": 1781218942140944393,
  "callbackUrl": "https://open.cloud-dahua.com/message/api-message/test/get",
  "isPush": true
}
```

#### 按回调配置ID搜索回调配置信息

**接口说明**: 获取回调配置信息。

**URL**: `/open-api/api-message/getInfoByCallbackConfigId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| callbackConfigId | number | 是 | 回调配置ID |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.isPush | boolean | 否 | 是否开启消息推送 |
| data.callbackUrl | string | 否 | 回调配置地址URL |
| data.callbackConfigId | number | 否 | 回调配置ID |
| data.productId | string | 否 | 产品ID |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackConfigId": 1881891823266938880
}
```

#### 添加回调配置信息

**接口说明**: 添加回调配置信息。

**URL**: `/open-api/api-message/addCallbackConfig`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 默认值 | 描述 |
|---------|---------|---------|--------|------|
| callbackUrl | string | 是 | | 回调配置地址URL |
| isPush | boolean | 否 | true | 是否开启消息推送：false关闭，true开启 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | number | 是 | 回调配置ID |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackUrl": "https://open.cloud-dahua.com/message/api-message/test/get",
  "isPush": true
}
```

**响应示例**:
```json
{
  "code": "200",
  "success": true,
  "data": 39875492375432451,
  "msg": "Operation is successful."
}
```

#### 按设备品类获取可订阅的消息类型

**接口说明**: 按设备品类获取支持的消息类型。

**URL**: `/open-api/api-message/getMessageTypePage`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 默认值 | 描述 |
|---------|---------|---------|--------|------|
| categoryCode | string | 是 | | 设备品类编码 |
| pageSize | number | 否 | 10 | 每页记录数（范围10-50） |
| pageNum | number | 否 | 1 | 页码 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| data.totalPage | number | 是 | 总页数 |
| data.pageSize | number | 是 | 每页记录数 |
| data.pageData | object[] | 是 | 当前页数据 |
| data.pageData[].code | string | 否 | 消息类型编码 |
| data.pageData[].createTime | string | 否 | 创建时间 |
| data.pageData[].memo | string | 否 | 消息说明 |
| data.pageData[].type | string | 否 | 消息类型 |
| data.totalRows | number | 是 | 总数量 |
| data.currentPage | number | 是 | 当前页码 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "categoryCode": "IPC",
  "pageNum": 1,
  "pageSize": 10
}
```

#### 根据回调配置ID更新设备订阅消息

**接口说明**: 根据CallbackConfigId和DeviceId修改订阅的MessageTypeCode。

**URL**: `/open-api/api-message/updateSubscribeByCallbackConfigId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| callbackConfigId | number | 是 | 回调配置ID |
| deviceId | string | 是 | 设备序列号 |
| messageTypeCodes | string[] | 否 | 消息类型列表 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackConfigId": 1781218942140944390,
  "deviceId": "B874AC****Q1002",
  "messageTypeCodes": ["online"]
}
```

#### 删除回调配置及相关订阅消息

**接口说明**: 根据CallbackConfigId删除消息时，删除与CallbackConfigId相关的设备的所有订阅消息。

**URL**: `/open-api/api-message/deleteCallbackId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| callbackConfigIds | number[] | 是 | 回调配置ID列表 |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object | 是 | 响应数据 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackConfigIds": ["1781218942140944390"]
}
```

#### 根据回调配置ID搜索已订阅的设备消息

**接口说明**: 根据回调配置ID搜索已订阅的设备消息。

**URL**: `/open-api/api-message/getSubscribeInfoByCallbackConfigId`

**请求参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| callbackConfigId | number | 是 | 回调配置ID |

**响应参数**:

| 参数名称 | 参数类型 | 是否必需 | 描述 |
|---------|---------|---------|------|
| code | string | 是 | 响应状态代码 |
| success | boolean | 是 | 响应状态 |
| data | object[] | 是 | 响应数据 |
| data[].deviceId | string | 否 | 设备序列号 |
| data[].messageTypeCodes | string[] | 否 | 消息类型列表 |
| msg | string | 是 | 响应消息 |

**请求示例**:
```json
{
  "callbackConfigId": 1781218942140944390
}
```

## 通道类型枚举

| unitType | 描述 |
|----------|------|
| 1 | 通用视频通道 |
| 2 | 解码单元 |
| 3 | 报警输入通道 |
| 4 | 报警输出通道 |
| 5 | 大屏输入单元 |
| 6 | 大屏输出单元 |
| 7 | 门禁单元 |
| 8 | 声卡单元 |
| 9 | 转码单元 |
| 12 | 消防通道 |
| 13 | 消防主机单元 |
| 14 | 道闸单元 |
| 15 | 动环外设单元 |
| 16 | 警号通道 |
| 17 | 融合通道 |
| 99 | 模拟通道 |

## 设备使能类型表

| abilityType | 应用级别 |
|-------------|----------|
| localRecord | channel |
| motionDetect | channel |
| faceCapture | channel |
| speechRecognition | device |
| breathingLight | device |
| smartLocate | all |
| smartTrack | all |
| localAlarmRecord | channel |
| regularCruise | all |
| headerDetect | channel |
| numberStat | channel |
| manNumDec | channel |
| alarmPIR | channel |
| autoZoomFocus | channel |
| audioEncodeControl | channel |
| aecv3 | channel |
| faceDetect | channel |
| localStorageEnable | device |
| whiteLight | all |
| linkageWhiteLight | channel |
| linkageSiren | all |
| infraredLight | device |
| searchLight | device |
| hoveringAlarm | channel |
| beOpenedDoor | device |
| closeCamera | channel |
| mobileDetect | channel |
| rtFaceDetect | channel |
| rtFaceCompa | channel |
| closeDormant | device |
| heatMap | channel |
| tlsEnable | device |
| aiHumanCar | channel |
| aiHuman | channel |
| aiCar | channel |
| openDoorByFace | device |
| openDoorByTouch | device |
| linkDevAlarm | channel |
| linkAccDevAlarm | device |
| abAlarmSound | channel |
| playSound | device |
| wideDynamic | channel |
| smdHuman | channel |
| smdVehicle | channel |
| instantDisAlarm | device |
| periodDisAlarm | device |
| ccss | channel |
| inll | channel |
| ledsw | device |


path: scripts/dahua_iot_client.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大华云开放平台 - 基础通用套件客户端

该脚本提供基础通用套件的完整客户端实现，包括：
- 认证与签名
- 设备管理
- SD卡管理
- 设备配置
- 消息订阅

Required environment variables:
  - DAHUA_CLOUD_PRODUCT_ID: AppID from Dahua Cloud
  - DAHUA_CLOUD_AK: Access Key from Dahua Cloud
  - DAHUA_CLOUD_SK: Secret Key from Dahua Cloud

Required dependency:
  - requests (pip install requests)
  - pycryptodome (仅 AES256 加密时需要，Base64 无需此依赖)

Usage:
  python dahua_iot_client.py --help
"""

import sys
import os
import argparse
import io
import json
import base64
import hashlib
import hmac
import time
import uuid
from pathlib import Path
from typing import Optional, Dict, Any, List
import requests


# ============================================================================
# Constants
# ============================================================================

DEFAULT_API_BASE_URL = 'https://open.cloud-dahua.com/'
TOKEN_EXPIRY_SECONDS = 604800  # 7天

# API Endpoints - 鉴权认证
API_AUTH_TOKEN = '/open-api/api-base/auth/getAppAccessToken'

# API Endpoints - 设备管理
API_ADD_DEVICE = '/open-api/api-iot/device/addDevice'
API_ADD_GB_DEVICE = '/open-api/api-iot/device/addGbDevice'
API_DELETE_DEVICE = '/open-api/api-iot/device/deleteDevice'
API_GET_DEVICE_LIST = '/open-api/api-iot/device/getDeviceList'
API_LIST_DEVICE_DETAILS = '/open-api/api-iot/device/listDeviceDetailsByIds'
API_DEVICE_ONLINE = '/open-api/api-iot/device/deviceOnline'
API_CHECK_DEVICE_BIND = '/open-api/api-iot/device/checkDeviceBindInfo'
API_GET_CATEGORY = '/open-api/api-iot/device/getCategory'

# API Endpoints - 国标设备
API_LIST_GB_CODE = '/open-api/api-iot/device/listGbCode'
API_GET_SIP_INFO = '/open-api/api-iot/device/getSipInfo'
API_GB_DEVICE_INFO_DETAIL = '/open-api/api-iot/device/deviceInfoDetailAll'
API_MODIFY_GB_DEVICE = '/open-api/api-iot/device/modifyGbDevice'

# API Endpoints - 设备配置
API_MODIFY_DEVICE_NAME = '/open-api/api-iot/device/modifyDeviceName'
API_MODIFY_DEV_CODE = '/open-api/api-iot/device/modifyDevCode'
API_VERIFY_DEV_CODE = '/open-api/api-iot/device/verifyDevCode'
API_GET_ABILITY_STATUS = '/open-api/api-iot/device/getAbilityStatus'
API_SET_ABILITY_STATUS = '/open-api/api-iot/device/setAbilityStatus'
API_SET_CURRENT_UTC = '/open-api/api-aiot/device/setCurrentUTC'

# API Endpoints - SD卡管理
API_FORMAT_SD_CARD = '/open-api/api-aiot/device/formatSDCard'
API_GET_SD_CARD_STATUS = '/open-api/api-aiot/device/getSDCardStatus'
API_GET_SD_CARD_STORAGE = '/open-api/api-aiot/device/getSDCardStorage'
API_LIST_SD_CARD_STORAGE = '/open-api/api-aiot/device/listSDCardStorage'
API_GET_DEVICE_CHANNEL_INFO = '/open-api/api-aiot/device/getDeviceChannelInfo'

# API Endpoints - 铃音配置
API_ADD_CUSTOM_RING = '/open-api/api-aiot/device/addCustomRing'
API_DELETE_CUSTOM_RING = '/open-api/api-aiot/device/deleteCustomRing'
API_LIST_CUSTOM_RING = '/open-api/api-aiot/device/listCustomRing'
API_SET_CUSTOM_RING = '/open-api/api-aiot/device/setCustomRing'

# API Endpoints - 网络配置
API_WIFI_AROUND = '/open-api/api-aiot/device/wifiAround'
API_CURRENT_DEVICE_WIFI = '/open-api/api-aiot/device/currentDeviceWifi'
API_CONTROL_DEVICE_WIFI = '/open-api/api-aiot/device/controlDeviceWifi'

# API Endpoints - 消息订阅
API_ADD_CALLBACK_CONFIG = '/open-api/api-message/addCallbackConfig'
API_GET_ALL_CALLBACK_CONFIG = '/open-api/api-message/getAllCallbackConfigId'
API_MESSAGE_SUBSCRIBE = '/open-api/api-message/messageSubscribeByDeviceIds'
API_GET_MESSAGE_TYPE_PAGE = '/open-api/api-message/getMessageTypePage'
API_GET_SUBSCRIBE_INFO = '/open-api/api-message/getMessageSubscribeInfoByDeviceId'
API_UPDATE_SUBSCRIBE_BY_CALLBACK = '/open-api/api-message/updateSubscribeByCallbackConfigId'
API_UPDATE_BY_CALLBACK_CONFIG = '/open-api/api-message/updateByCallbackConfigId'
API_DELETE_CALLBACK_ID = '/open-api/api-message/deleteCallbackId'
API_GET_INFO_BY_CALLBACK_CONFIG = '/open-api/api-message/getInfoByCallbackConfigId'
API_GET_SUBSCRIBE_INFO_BY_CALLBACK = '/open-api/api-message/getSubscribeInfoByCallbackConfigId'

# API Endpoints - 图片解密
API_IMAGE_DECRYPTO = '/open-api/api-decrypto/image/decrypto'

# API Endpoints - SIM
API_GET_SIM_SIGNAL_STRENGTH = '/open-api/api-aiot/device/getSimSignalStrength'

# HTTP Timeouts (seconds)
TIMEOUT_AUTH = 60
TIMEOUT_DEVICE = 60
TIMEOUT_CONFIG = 60
TIMEOUT_SD_CARD = 60
TIMEOUT_FORMAT_SD = 120  # 格式化SD卡可能较慢

# Environment Variable Names
ENV_CLOUD_ID = 'DAHUA_CLOUD_PRODUCT_ID'
ENV_CLOUD_AK = 'DAHUA_CLOUD_AK'
ENV_CLOUD_SK = 'DAHUA_CLOUD_SK'


def fix_encoding():
    """Fix encoding for Windows PowerShell/CMD to ensure proper display"""
    if sys.platform == 'win32':
        if sys.stdout.encoding != 'utf-8':
            sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
        if sys.stderr.encoding != 'utf-8':
            sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')


# ============================================================================
# Signature Functions
# ============================================================================

def get_token_sign(access_key: str, timestamp: str, nonce: str, secret: str) -> str:
    """
    Auth API signature (for getting Token)
    Signature factor: access_key + timestamp + nonce
    """
    auth_factor = f'{access_key}{timestamp}{nonce}'
    return hmac.new(
        secret.encode('utf-8'),
        auth_factor.encode('utf-8'),
        hashlib.sha512
    ).hexdigest().upper()


def business_api_sign(access_key: str, app_access_token: str, timestamp: str, nonce: str, secret: str) -> str:
    """
    Business API signature (for all device management APIs)
    Signature factor: access_key + app_access_token + timestamp + nonce
    """
    auth_factor = f'{access_key}{app_access_token}{timestamp}{nonce}'
    return hmac.new(
        secret.encode('utf-8'),
        auth_factor.encode('utf-8'),
        hashlib.sha512
    ).hexdigest().upper()


# ============================================================================
# Device Code Encryption
# ============================================================================

class DeviceCodeEncryptor:
    """设备密码加密工具类"""
    
    # AES加密的固定IV值
    IV = b'86E2DB6D77B5E9CD'
    
    def __init__(self, secret_access_key: str, device_id: Optional[str] = None):
        """
        初始化加密工具
        
        Args:
            secret_access_key: 产品的SecretAccessKey
            device_id: 设备序列号（可选）
        """
        self.secret_access_key = secret_access_key
        self.device_id = device_id
    
    def _generate_aes_key(self) -> bytes:
        """
        生成AES密钥
        算法：Cut32(UpperCase(MD5-32位(UpperCase(sk))))
        """
        # 计算MD5-32位
        md5_hash = hashlib.md5(self.secret_access_key.upper().encode('utf-8')).hexdigest().upper()
        # 截取前32位
        aes_key = md5_hash[:32]
        return aes_key.encode('utf-8')
    
    def encrypt_base64(self, device_password: str) -> str:
        """
        方式一：Base64加密设备密码
        格式："Dolynk_" + Base64(设备密码)
        """
        encoded = base64.b64encode(device_password.encode('utf-8')).decode('utf-8')
        return f"Dolynk_{encoded}"
    
    def encrypt_aes256(self, device_password: str) -> str:
        """
        方式二：AES256加密设备密码
        格式：Base64(Aes256(待加密内容, AesKey, IV初始向量))
        加密算法：Aes256/CBC/PKCS7
        需要 pycryptodome 库：pip install pycryptodome
        """
        try:
            from Crypto.Cipher import AES
            from Crypto.Util.Padding import pad
        except ImportError:
            raise RuntimeError(
                'AES256 加密需要 pycryptodome 库。请执行: pip install pycryptodome\n'
                '若无法安装，可使用 Base64 加密: python dahua_iot_client.py add -d <设备ID> -p <密码> -e base64'
            )
        # 生成AES密钥
        aes_key = self._generate_aes_key()
        # 创建AES加密器（CBC模式）
        cipher = AES.new(aes_key, AES.MODE_CBC, self.IV)
        # PKCS7填充并加密
        encrypted = cipher.encrypt(pad(device_password.encode('utf-8'), AES.block_size))
        # Base64编码
        encoded = base64.b64encode(encrypted).decode('utf-8')
        return encoded


# ============================================================================
# Dahua IoT Client
# ============================================================================

class DahuaIoTClient:
    """大华云开放平台 - 基础通用套件客户端"""
    
    def __init__(
        self,
        app_id: str,
        access_key: str,
        secret_key: str,
        api_base_url: str = DEFAULT_API_BASE_URL,
        verbose: bool = True
    ):
        """
        初始化客户端
        
        Args:
            app_id: 产品ID (ProductId)
            access_key: 访问密钥 (AccessKey)
            secret_key: 密钥 (SecretKey)
            api_base_url: API基础地址
            verbose: 是否打印API调用日志（SDK集成时建议设为False）
        """
        self.config = {
            'app_id': app_id,
            'access_key': access_key,
            'secret_key': secret_key,
            'api_base_url': api_base_url.rstrip('/'),
            'verbose': verbose
        }
        self.session = requests.Session()
        self.access_token: Optional[str] = None
        self.token_expiry: int = 0
        self.encryptor = DeviceCodeEncryptor(secret_key)
    
    def _log(self, *args, **kwargs):
        """条件打印（verbose=True时输出）"""
        if self.config.get('verbose', True):
            print(*args, **kwargs)

    def _get_app_access_token(self) -> Optional[str]:
        """获取AppAccessToken"""
        self._log("[Auth] Getting AppAccessToken...")
        
        try:
            timestamp = str(int(time.time() * 1000))
            nonce = str(uuid.uuid4())
            trace_id = f"tid-{int(time.time())}"
            
            # 生成签名
            signature = get_token_sign(
                self.config['access_key'],
                timestamp,
                nonce,
                self.config['secret_key']
            )
            
            headers = {
                'Content-Type': 'application/json',
                'Accept-Language': 'zh-CN',
                'AccessKey': self.config['access_key'],
                'Timestamp': timestamp,
                'Nonce': nonce,
                'Sign': signature,
                'ProductId': self.config['app_id'],
                'X-TraceId-Header': trace_id,
                'Version': 'V1',
                'Sign-Type': 'simple'
            }
            
            payload = {'productId': self.config['app_id']}
            
            url = self.config['api_base_url'] + API_AUTH_TOKEN
            response = self.session.post(url, headers=headers, json=payload, timeout=TIMEOUT_AUTH)
            
            result = response.json()
            
            if result.get('success'):
                self.access_token = result.get('data', {}).get('appAccessToken')
                self.token_expiry = int(time.time()) + TOKEN_EXPIRY_SECONDS
                self._log(f"[OK] Token obtained successfully (expires in {TOKEN_EXPIRY_SECONDS}s)")
                return self.access_token
            else:
                self._log(f"[ERROR] Failed to get [REDACTED_SECRET]'msg')}")
                return None
                
        except Exception as e:
            self._log(f"[EXCEPTION] Auth failed: {e}")
            return None
    
    def _ensure_token(self) -> bool:
        """确保Token有效"""
        current_time = int(time.time())
        if not self.access_token or current_time > self.token_expiry:
            return self._get_app_access_token() is not None
        return True
    
    def _build_headers(self) -> Dict[str, str]:
        """构建业务API请求头"""
        if not self._ensure_token():
            raise ValueError("Failed to get access token")
        
        timestamp = str(int(time.time() * 1000))
        nonce = str(uuid.uuid4())
        trace_id = f"tid-{int(time.time())}"
        
        # 生成签名
        signature = business_api_sign(
            self.config['access_key'],
            self.access_token,
            timestamp,
            nonce,
            self.config['secret_key']
        )
        
        return {
            'Content-Type': 'application/json',
            'Accept-Language': 'zh-CN',
            'AccessKey': self.config['access_key'],
            'Timestamp': timestamp,
            'Nonce': nonce,
            'Sign': signature,
            'ProductId': self.config['app_id'],
            'X-TraceId-Header': trace_id,
            'Version': 'V1',
            'Sign-Type': 'simple',
            'AppAccessToken': self.access_token
        }
    
    def _request(self, endpoint: str, data: Dict[str, Any], timeout: int = TIMEOUT_DEVICE) -> Dict[str, Any]:
        """
        发送API请求
        
        Args:
            endpoint: API端点
            data: 请求数据
            timeout: 超时时间
            
        Returns:
            API响应
        """
        try:
            headers = self._build_headers()
            url = self.config['api_base_url'] + endpoint
            
            self._log(f"[API] POST {endpoint}")
            self._log(f"[DATA] {json.dumps(data, ensure_ascii=False)}")
            
            response = self.session.post(url, headers=headers, json=data, timeout=timeout)
            result = response.json()
            
            resp_preview = json.dumps(result, ensure_ascii=False)
            self._log(f"[RESP] {resp_preview[:200]}{'...' if len(resp_preview) > 200 else ''}")
            
            return result
            
        except Exception as e:
            self._log(f"[EXCEPTION] Request failed: {e}")
            return {'success': False, 'msg': str(e)}
    
    # ========================================================================
    # 设备管理
    # ========================================================================
    
    def add_device(
        self,
        device_id: str,
        device_password: str,
        category_code: str = 'IPC',
        encrypt_method: str = 'base64'
    ) -> Dict[str, Any]:
        """
        添加设备
        
        Args:
            device_id: 设备序列号
            device_password: 设备密码（明文）
            category_code: 设备品类编码
            encrypt_method: 加密方式 ('base64' 或 'aes256')
            
        Returns:
            API响应
        """
        self._log(f"\n[Device] Adding device: {device_id}")
        
        # 加密设备密码
        if encrypt_method == 'aes256':
            dev_code = self.encryptor.encrypt_aes256(device_password)
        else:
            dev_code = self.encryptor.encrypt_base64(device_password)
        
        data = {
            'deviceId': device_id,
            'devCode': dev_code,
            'categoryCode': category_code
        }
        
        return self._request(API_ADD_DEVICE, data)
    
    def delete_device(self, device_id: str) -> Dict[str, Any]:
        """
        删除设备
        
        Args:
            device_id: 设备序列号
            
        Returns:
            API响应
        """
        self._log(f"\n[Device] Deleting device: {device_id}")
        
        data = {'deviceId': device_id}
        return self._request(API_DELETE_DEVICE, data)
    
    def verify_device_password(
        self,
        device_id: str,
        device_password: str,
        encrypt_method: str = 'base64'
    ) -> Dict[str, Any]:
        """
        验证设备密码是否正确
        
        Args:
            device_id: 设备序列号
            device_password: 设备密码（明文）
            encrypt_method: 加密方式 ('base64' 或 'aes256')
            
        Returns:
            API响应，success 为 True 表示密码正确
        """
        self._log(f"\n[Device] Verifying password for: {device_id}")
        
        if encrypt_method == 'aes256':
            dev_code = self.encryptor.encrypt_aes256(device_password)
        else:
            dev_code = self.encryptor.encrypt_base64(device_password)
        
        data = {'deviceId': device_id, 'devCode': dev_code}
        return self._request(API_VERIFY_DEV_CODE, data)
    
    def get_device_list(
        self,
        page_num: int = 1,
        page_size: int = 10,
        device_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        查询设备列表
        
        Args:
            page_num: 页码
            page_size: 每页条数
            device_id: 设备序列号（支持模糊查询）
            
        Returns:
            API响应
        """
        self._log(f"\n[Device] Getting device list (page {page_num}, size {page_size})")
        
        data = {
            'pageNum': page_num,
            'pageSize': page_size
        }
        
        if device_id:
            data['deviceId'] = device_id
        
        return self._request(API_GET_DEVICE_LIST, data)
    
    def get_device_online(self, device_id: str) -> Dict[str, Any]:
        """
        获取设备在线状态
        
        Args:
            device_id: 设备序列号
            
        Returns:
            API响应
        """
        self._log(f"\n[Device] Getting online status: {device_id}")
        
        data = {'deviceId': device_id}
        return self._request(API_DEVICE_ONLINE, data)
    
    def check_device_bind(self, device_id: str) -> Dict[str, Any]:
        """
        查询设备绑定状态
        
        Args:
            device_id: 设备序列号
            
        Returns:
            API响应，含 bindStatus、deviceExist、status
        """
        self._log(f"\n[Device] Checking bind status: {device_id}")
        
        data = {'deviceId': device_id}
        return self._request(API_CHECK_DEVICE_BIND, data)
    
    def list_device_details(self, device_ids: List[str]) -> Dict[str, Any]:
        """
        批量查询设备详细信息
        
        Args:
            device_ids: 设备序列号列表（最多100个）
            
        Returns:
            API响应
        """
        self._log(f"\n[Device] Getting details for {len(device_ids)} devices")
        
        data = {
            'deviceList': [{'deviceId': did} for did in device_ids]
        }
        
        return self._request(API_LIST_DEVICE_DETAILS, data)
    
    def get_device_channel_info(self, device_id: str) -> Dict[str, Any]:
        """
        获取设备通道信息
        
        查询设备通道下挂载设备的序列号、设备大类、设备型号信息。
        
        Args:
            device_id: 设备序列号
            
        Returns:
            API响应，含 channels 通道列表
        """
        self._log(f"\n[Device] Getting channel info: {device_id}")
        
        data = {'deviceId': device_id}
        return self._request(API_GET_DEVICE_CHANNEL_INFO, data)
    
    def get_category(
        self,
        primary_category_code: Optional[str] = None,
        second_category_code: Optional[str] = None,
        device_model: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        查询设备品类
        
        Args:
            primary_category_code: 1级设备类别代码
            second_category_code: 2级设备类别代码（如 IPC）
            device_model: 设备型号（仅支持每次传一个）
            
        Returns:
            API响应，含 categoryList
        """
        self._log(f"\n[Device] Getting device category list")
        
        data: Dict[str, Any] = {}
        if primary_category_code:
            data['primaryCategoryCode'] = primary_category_code
        if second_category_code:
            data['secondCategoryCode'] = second_category_code
        if device_model:
            data['deviceModel'] = device_model
        
        return self._request(API_GET_CATEGORY, data)
    
    # ========================================================================
    # 国标设备
    # ========================================================================
    
    def list_gb_code(
        self,
        count: int = 10,
        prefix: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        获取国标码列表
        
        Args:
            count: 单次查询数量（1-10000）
            prefix: 自定义国标码前缀（长度必须13位）
            
        Returns:
            API响应，含 minGbCode、maxGbCode
        """
        self._log(f"\n[GB] Getting GB code list (count={count})")
        
        data = {'count': count}
        if prefix:
            data['prefix'] = prefix
        
        return self._request(API_LIST_GB_CODE, data)
    
    def get_sip_info(self) -> Dict[str, Any]:
        """
        获取国标设备注册信息（Sip服务器IP和端口号）
        
        Returns:
            API响应，含 sipServerIp
        """
        self._log(f"\n[GB] Getting SIP registration info")
        
        return self._request(API_GET_SIP_INFO, {})
    
    def add_gb_device(
        self,
        gb_code: str,
        channel_number: int,
        device_password: Optional[str] = None,
        dev_code: Optional[str] = None,
        encrypt_method: str = 'base64',
        manufacturer: Optional[str] = None,
        gb_stream_model: Optional[str] = None,
        device_class: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        添加国标设备
        
        Args:
            gb_code: 国标码
            channel_number: 通道总数
            device_password: 设备注册密码（明文），与 dev_code 二选一
            dev_code: 已加密的注册密码，与 device_password 二选一
            encrypt_method: 密码加密方式（device_password 时有效，base64/aes256）
            manufacturer: 厂商（Dahua、HIKVSION、UNKNOW）
            gb_stream_model: 拉流协议（TCP 或 UDP，默认 UDP）
            device_class: 设备类型（NVR 或 IPC，默认 NVR）
            
        Returns:
            API响应，含 deviceId 等
        """
        self._log(f"\n[GB] Adding GB device: {gb_code}")
        
        if dev_code:
            enc = dev_code
        elif device_password:
            if encrypt_method == 'aes256':
                enc = self.encryptor.encrypt_aes256(device_password)
            else:
                enc = self.encryptor.encrypt_base64(device_password)
        else:
            raise ValueError('必须提供 device_password 或 dev_code')
        
        data = {
            'gbCode': gb_code,
            'devCode': enc,
            'channelNumber': channel_number
        }
        if manufacturer:
            data['manufacturer'] = manufacturer
        if gb_stream_model:
            data['gbStreamModel'] = gb_stream_model
        if device_class:
            data['deviceClass'] = device_class
        
        return self._request(API_ADD_GB_DEVICE, data)
    
    def list_gb_device_details(self, device_ids: List[str]) -> Dict[str, Any]:
        """
        查询国标设备详细信息
        
        Args:
            device_ids: 设备序列号列表
            
        Returns:
            API响应，含国标设备详细信息和通道信息
        """
        self._log(f"\n[GB] Getting details for {len(device_ids)} GB device(s)")
        
        data = {'deviceIds': device_ids}
        return self._request(API_GB_DEVICE_INFO_DETAIL, data)
    
    def modify_gb_device(
        self,
        device_id: str,
        device_password: Optional[str] = None,
        dev_code: Optional[str] = None,
        encrypt_method: str = 'base64',
        gb_stream_model: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        修改国标设备信息
        
        Args:
            device_id: 设备序列号
            device_password: 新密码（明文），与 dev_code 二选一
            dev_code: 已加密的新密码，与 device_password 二选一
            encrypt_method: 密码加密方式（device_password 时有效）
            gb_stream_model: 拉流协议（TCP 或 UDP）
            
        Returns:
            API响应
        """
        self._log(f"\n[GB] Modifying GB device: {device_id}")
        
        data = {'deviceId': device_id}
        
        if dev_code:
            data['devCode'] = dev_code
        elif device_password:
            if encrypt_method == 'aes256':
                data['devCode'] = self.encryptor.encrypt_aes256(device_password)
            else:
                data['devCode'] = self.encryptor.encrypt_base64(device_password)
        
        if gb_stream_model:
            data['gbStreamModel'] = gb_stream_model
        
        return self._request(API_MODIFY_GB_DEVICE, data)
    
    # ========================================================================
    # SD卡管理
    # ========================================================================
    
    def get_sd_card_status(self, device_id: str, index: Optional[int] = None) -> Dict[str, Any]:
        """
        获取SD卡状态
        
        Args:
            device_id: 设备序列号
            index: SD卡编号（多卡设备需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[SD Card] Getting status for device: {device_id}")
        
        data = {'deviceId': device_id}
        if index is not None:
            data['index'] = index
        
        return self._request(API_GET_SD_CARD_STATUS, data)
    
    def get_sd_card_storage(self, device_id: str, index: Optional[int] = None) -> Dict[str, Any]:
        """
        查询SD卡容量
        
        Args:
            device_id: 设备序列号
            index: SD卡编号（多卡设备需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[SD Card] Getting storage for device: {device_id}")
        
        data = {'deviceId': device_id}
        if index is not None:
            data['index'] = index
        
        return self._request(API_GET_SD_CARD_STORAGE, data)
    
    def format_sd_card(self, device_id: str, index: Optional[int] = None) -> Dict[str, Any]:
        """
        格式化SD卡
        
        Args:
            device_id: 设备序列号
            index: SD卡编号（多卡设备需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[SD Card] Formatting SD card for device: {device_id}")
        
        data = {'deviceId': device_id}
        if index is not None:
            data['index'] = index
        
        return self._request(API_FORMAT_SD_CARD, data, timeout=TIMEOUT_FORMAT_SD)
    
    def list_sd_card_storage(self, device_id: str) -> Dict[str, Any]:
        """获取设备SD卡列表（多卡设备）"""
        self._log(f"\n[SD Card] Getting SD card list for device: {device_id}")
        data = {'deviceId': device_id}
        return self._request(API_LIST_SD_CARD_STORAGE, data)
    
    # ========================================================================
    # 铃音配置
    # ========================================================================
    
    def add_custom_ring(
        self,
        device_id: str,
        name: str,
        url: str,
        ring_type: str,
        channel_id: Optional[str] = None,
        relate_type: Optional[str] = None,
        language: Optional[str] = None,
        version: Optional[str] = None
    ) -> Dict[str, Any]:
        """新增自定义铃声"""
        self._log(f"\n[Ring] Adding custom ring: {name}")
        data = {'deviceId': device_id, 'name': name, 'url': url, 'type': ring_type}
        if channel_id:
            data['channelId'] = channel_id
        if relate_type:
            data['relateType'] = relate_type
        if language:
            data['language'] = language
        if version:
            data['version'] = version
        return self._request(API_ADD_CUSTOM_RING, data)
    
    def delete_custom_ring(
        self,
        device_id: str,
        index: str,
        relate_type: str
    ) -> Dict[str, Any]:
        """删除自定义铃声"""
        self._log(f"\n[Ring] Deleting custom ring: {index}")
        data = {'deviceId': device_id, 'index': index, 'relateType': relate_type}
        return self._request(API_DELETE_CUSTOM_RING, data)
    
    def list_custom_ring(
        self,
        device_id: str,
        relate_type: str
    ) -> Dict[str, Any]:
        """获取铃声列表"""
        self._log(f"\n[Ring] Getting custom ring list for device: {device_id}")
        data = {'deviceId': device_id, 'relateType': relate_type}
        return self._request(API_LIST_CUSTOM_RING, data)
    
    def set_custom_ring(
        self,
        device_id: str,
        index: str,
        relate_type: str,
        channel_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """设置铃声"""
        self._log(f"\n[Ring] Setting custom ring: {index}")
        data = {'deviceId': device_id, 'index': index, 'relateType': relate_type}
        if channel_id:
            data['channelId'] = channel_id
        return self._request(API_SET_CUSTOM_RING, data)
    
    # ========================================================================
    # 网络配置
    # ========================================================================
    
    def wifi_around(self, device_id: str) -> Dict[str, Any]:
        """获取设备周边热点信息"""
        self._log(f"\n[WiFi] Getting wifi around for device: {device_id}")
        data = {'deviceId': device_id}
        return self._request(API_WIFI_AROUND, data)
    
    def current_device_wifi(self, device_id: str) -> Dict[str, Any]:
        """获取设备当前连接的热点信息"""
        self._log(f"\n[WiFi] Getting current wifi for device: {device_id}")
        data = {'deviceId': device_id}
        return self._request(API_CURRENT_DEVICE_WIFI, data)
    
    def control_device_wifi(
        self,
        device_id: str,
        ssid: str,
        bssid: Optional[str] = None,
        [REDACTED_SECRET] = None,
        link_enable: Optional[bool] = None,
        channel_sn: Optional[str] = None
    ) -> Dict[str, Any]:
        """修改设备连接热点"""
        self._log(f"\n[WiFi] Controlling device wifi: {device_id}")
        data = {'deviceId': device_id, 'ssid': ssid}
        if bssid:
            data['bssid'] = bssid
        if password:
            data['password'] = password
        if link_enable is not None:
            data['linkEnable'] = link_enable
        if channel_sn:
            data['channelSn'] = channel_sn
        return self._request(API_CONTROL_DEVICE_WIFI, data)
    
    def get_sim_signal_strength(self, device_id: str) -> Dict[str, Any]:
        """获取SIM信号强度"""
        self._log(f"\n[Device] Getting SIM signal strength: {device_id}")
        data = {'deviceId': device_id}
        return self._request(API_GET_SIM_SIGNAL_STRENGTH, data)
    
    # ========================================================================
    # 设备配置
    # ========================================================================
    
    def modify_device_name(
        self,
        device_id: str,
        name: str,
        channel_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        修改设备或通道名称
        
        Args:
            device_id: 设备序列号
            name: 新名称
            channel_id: 通道号（修改通道名称时需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[Config] Modifying name for device: {device_id}")
        
        data = {
            'deviceId': device_id,
            'name': name
        }
        
        if channel_id:
            data['channelId'] = channel_id
        
        return self._request(API_MODIFY_DEVICE_NAME, data)
    
    def get_ability_status(
        self,
        device_id: str,
        ability_type: str,
        channel_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        获取设备使能开关状态
        
        Args:
            device_id: 设备序列号
            ability_type: 使能类型
            channel_id: 通道号（通道级使能需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[Config] Getting ability status: {ability_type}")
        
        data = {
            'deviceId': device_id,
            'abilityType': ability_type
        }
        
        if channel_id:
            data['channelId'] = channel_id
        
        return self._request(API_GET_ABILITY_STATUS, data)
    
    def set_ability_status(
        self,
        device_id: str,
        ability_type: str,
        status: str,
        channel_id: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        设置设备使能开关
        
        Args:
            device_id: 设备序列号
            ability_type: 使能类型
            status: 状态 ('on' 或 'off')
            channel_id: 通道号（通道级使能需要）
            
        Returns:
            API响应
        """
        self._log(f"\n[Config] Setting ability {ability_type} to {status}")
        
        data = {
            'deviceId': device_id,
            'abilityType': ability_type,
            'status': status
        }
        
        if channel_id:
            data['channelId'] = channel_id
        
        return self._request(API_SET_ABILITY_STATUS, data)
    
    def modify_dev_code(
        self,
        device_id: str,
        old_password: str,
        new_password: str,
        encrypt_method: str = 'base64'
    ) -> Dict[str, Any]:
        """修改设备密码"""
        self._log(f"\n[Config] Modifying device [REDACTED_SECRET]")
        if encrypt_method == 'aes256':
            old_code = self.encryptor.encrypt_aes256(old_password)
            new_code = self.encryptor.encrypt_aes256(new_password)
        else:
            old_code = self.encryptor.encrypt_base64(old_password)
            new_code = self.encryptor.encrypt_base64(new_password)
        data = {'deviceId': device_id, 'oldDevCode': old_code, 'newDevCode': new_code}
        return self._request(API_MODIFY_DEV_CODE, data)
    
    def set_current_utc(
        self,
        device_id: str,
        utc: int,
        tolerance: int = 5
    ) -> Dict[str, Any]:
        """设备校时"""
        self._log(f"\n[Config] Setting device time: {device_id}")
        data = {'deviceId': device_id, 'utc': utc, 'tolerance': tolerance}
        return self._request(API_SET_CURRENT_UTC, data)
    
    # ========================================================================
    # 消息订阅
    # ========================================================================
    
    def add_callback_config(
        self,
        callback_url: str,
        is_push: bool = True
    ) -> Dict[str, Any]:
        """
        添加回调配置
        
        Args:
            callback_url: 回调地址
            is_push: 是否开启推送
            
        Returns:
            API响应
        """
        self._log(f"\n[Message] Adding callback config: {callback_url}")
        
        data = {
            'callbackUrl': callback_url,
            'isPush': is_push
        }
        
        return self._request(API_ADD_CALLBACK_CONFIG, data)
    
    def message_subscribe(
        self,
        device_ids: List[str],
        message_type_codes: List[str],
        category_code: str,
        callback_config_id: int
    ) -> Dict[str, Any]:
        """
        订阅设备消息
        
        Args:
            device_ids: 设备序列号列表（最多50个）
            message_type_codes: 消息类型列表
            category_code: 设备品类编码
            callback_config_id: 回调配置ID
            
        Returns:
            API响应
        """
        self._log(f"\n[Message] Subscribing {len(device_ids)} devices to {len(message_type_codes)} message types")
        
        data = {
            'deviceIds': device_ids,
            'messageTypeCodes': message_type_codes,
            'categoryCode': category_code,
            'callbackConfigId': callback_config_id
        }
        
        return self._request(API_MESSAGE_SUBSCRIBE, data)
    
    def get_all_callback_config(
        self,
        page_num: int = 1,
        page_size: int = 20
    ) -> Dict[str, Any]:
        """分页获取回调配置ID和回调配置地址"""
        self._log(f"\n[Message] Getting callback config list (page {page_num})")
        data = {'pageNum': page_num, 'pageSize': page_size}
        return self._request(API_GET_ALL_CALLBACK_CONFIG, data)
    
    def get_subscribe_info_by_device(self, device_id: str) -> Dict[str, Any]:
        """根据设备ID查询消息订阅信息"""
        self._log(f"\n[Message] Getting subscribe info for device: {device_id}")
        data = {'deviceId': device_id}
        return self._request(API_GET_SUBSCRIBE_INFO, data)
    
    def get_message_type_page(
        self,
        category_code: str,
        page_num: int = 1,
        page_size: int = 10
    ) -> Dict[str, Any]:
        """按设备品类获取可订阅的消息类型"""
        self._log(f"\n[Message] Getting message types for category: {category_code}")
        data = {'categoryCode': category_code, 'pageNum': page_num, 'pageSize': page_size}
        return self._request(API_GET_MESSAGE_TYPE_PAGE, data)
    
    def update_subscribe_by_callback_config(
        self,
        callback_config_id: int,
        device_id: str,
        message_type_codes: Optional[List[str]] = None
    ) -> Dict[str, Any]:
        """根据回调配置ID更新设备订阅消息"""
        self._log(f"\n[Message] Updating subscribe for callback {callback_config_id}")
        data = {'callbackConfigId': callback_config_id, 'deviceId': device_id}
        if message_type_codes:
            data['messageTypeCodes'] = message_type_codes
        return self._request(API_UPDATE_SUBSCRIBE_BY_CALLBACK, data)
    
    def update_callback_config(
        self,
        callback_config_id: int,
        callback_url: Optional[str] = None,
        is_push: Optional[bool] = None
    ) -> Dict[str, Any]:
        """按回调配置ID更新回调配置"""
        self._log(f"\n[Message] Updating callback config: {callback_config_id}")
        data = {'callbackConfigId': callback_config_id}
        if callback_url is not None:
            data['callbackUrl'] = callback_url
        if is_push is not None:
            data['isPush'] = is_push
        return self._request(API_UPDATE_BY_CALLBACK_CONFIG, data)
    
    def delete_callback_config(self, callback_config_ids: List[int]) -> Dict[str, Any]:
        """删除回调配置及相关订阅消息"""
        self._log(f"\n[Message] Deleting callback configs: {callback_config_ids}")
        data = {'callbackConfigIds': callback_config_ids}
        return self._request(API_DELETE_CALLBACK_ID, data)
    
    def get_callback_config_info(self, callback_config_id: int) -> Dict[str, Any]:
        """按回调配置ID搜索回调配置信息"""
        self._log(f"\n[Message] Getting callback config info: {callback_config_id}")
        data = {'callbackConfigId': callback_config_id}
        return self._request(API_GET_INFO_BY_CALLBACK_CONFIG, data)
    
    def get_subscribe_info_by_callback_config(
        self,
        callback_config_id: int
    ) -> Dict[str, Any]:
        """根据回调配置ID搜索已订阅的设备消息"""
        self._log(f"\n[Message] Getting subscribe info for callback: {callback_config_id}")
        data = {'callbackConfigId': callback_config_id}
        return self._request(API_GET_SUBSCRIBE_INFO_BY_CALLBACK, data)
    
    def image_decrypt(
        self,
        device_id: str,
        image_url: str,
        dev_code: Optional[str] = None,
        is_tcm_device: Optional[bool] = None
    ) -> Dict[str, Any]:
        """图片解密"""
        self._log(f"\n[Decrypto] Decrypting image for device: {device_id}")
        data = {'deviceId': device_id, 'imageUrl': image_url}
        if dev_code:
            data['devCode'] = dev_code
        if is_tcm_device is not None:
            data['isTcmDevice'] = is_tcm_device
        return self._request(API_IMAGE_DECRYPTO, data)


# ============================================================================
# Helper Functions
# ============================================================================

def create_client_from_env(verbose: bool = True) -> DahuaIoTClient:
    """从环境变量创建客户端"""
    app_id = os.environ.get(ENV_CLOUD_ID)
    access_key = os.environ.get(ENV_CLOUD_AK)
    secret_key = os.environ.get(ENV_CLOUD_SK)
    
    if not all([app_id, access_key, secret_key]):
        raise ValueError(
            f'Missing required environment variables!\n'
            f'Please set:\n'
            f'  {ENV_CLOUD_ID}\n'
            f'  {ENV_CLOUD_AK}\n'
            f'  {ENV_CLOUD_SK}'
        )
    
    return DahuaIoTClient(app_id, access_key, secret_key, verbose=verbose)


# ============================================================================
# Command Line Interface
# ============================================================================

def get_client(args):
    """获取客户端实例（仅当同时提供全部凭证参数时使用，否则使用环境变量）"""
    pid = getattr(args, 'product_id', None)
    ak = getattr(args, 'access_key', None)
    sk = getattr(args, 'secret_key', None)
    if pid and ak and sk:
        return DahuaIoTClient(app_id=pid, access_key=ak, secret_key=sk, verbose=True)
    return create_client_from_env(verbose=True)


def cmd_add_device(args):
    """添加设备命令"""
    client = get_client(args)
    result = client.add_device(
        device_id=args.device_id,
        device_password=args.password,
        category_code=args.category,
        encrypt_method=args.encrypt
    )
    print_result(result)


def cmd_add_gb_device(args):
    """添加国标设备命令"""
    client = get_client(args)
    result = client.add_gb_device(
        gb_code=args.gb_code,
        channel_number=args.channels,
        device_password=args.password,
        encrypt_method=args.encrypt,
        manufacturer=args.manufacturer,
        gb_stream_model=args.stream_model,
        device_class=args.device_class
    )
    print_result(result)


def cmd_delete_device(args):
    """删除设备命令"""
    client = get_client(args)
    result = client.delete_device(args.device_id)
    print_result(result)


def cmd_verify_device_password(args):
    """验证设备密码命令"""
    client = get_client(args)
    result = client.verify_device_password(
        device_id=args.device_id,
        device_password=args.password,
        encrypt_method=args.encrypt
    )
    print_result(result)
    if result.get('success'):
        print("\n✓ 密码正确")
    else:
        print("\n✗ 密码错误或验证失败")


def cmd_list_devices(args):
    """查询设备列表命令"""
    client = get_client(args)
    result = client.get_device_list(
        page_num=args.page,
        page_size=args.size,
        device_id=args.device_id
    )
    print_result(result)


def cmd_list_device_details(args):
    """查询设备详细信息命令"""
    client = get_client(args)
    result = client.list_device_details(args.device_ids)
    print_result(result)


def cmd_get_category(args):
    """查询设备品类命令"""
    client = get_client(args)
    result = client.get_category(
        primary_category_code=args.primary,
        second_category_code=args.second,
        device_model=args.model
    )
    print_result(result)


def cmd_device_online(args):
    """查询设备在线状态命令"""
    client = get_client(args)
    result = client.get_device_online(args.device_id)
    print_result(result)


def cmd_check_device_bind(args):
    """查询设备绑定状态命令"""
    client = get_client(args)
    result = client.check_device_bind(args.device_id)
    print_result(result)


def cmd_get_device_channel_info(args):
    """获取设备通道信息命令"""
    client = get_client(args)
    result = client.get_device_channel_info(args.device_id)
    print_result(result)


def cmd_get_ability_status(args):
    """获取设备使能状态命令"""
    client = get_client(args)
    result = client.get_ability_status(
        device_id=args.device_id,
        ability_type=args.ability_type,
        channel_id=args.channel_id
    )
    print_result(result)


def cmd_list_gb_code(args):
    """获取国标码列表命令"""
    client = get_client(args)
    result = client.list_gb_code(
        count=args.count,
        prefix=args.prefix
    )
    print_result(result)


def cmd_get_sip_info(args):
    """获取国标设备注册信息命令"""
    client = get_client(args)
    result = client.get_sip_info()
    print_result(result)


def cmd_list_gb_device_details(args):
    """查询国标设备详细信息命令"""
    client = get_client(args)
    result = client.list_gb_device_details(args.device_ids)
    print_result(result)


def cmd_modify_gb_device(args):
    """修改国标设备信息命令"""
    client = get_client(args)
    result = client.modify_gb_device(
        device_id=args.device_id,
        device_password=args.password,
        encrypt_method=args.encrypt,
        gb_stream_model=args.stream_model
    )
    print_result(result)


def cmd_encrypt_password(args):
    """加密设备密码命令"""
    encryptor = DeviceCodeEncryptor(args.secret_key, args.device_id)
    
    print(f"\n{'='*60}")
    print("Device Password Encryption")
    print(f"{'='*60}")
    print(f"Original [REDACTED_SECRET]")
    
    if args.method == 'aes256':
        encrypted = encryptor.encrypt_aes256(args.password)
        print(f"AES256 Encrypted: {encrypted}")
    else:
        encrypted = encryptor.encrypt_base64(args.password)
        print(f"Base64 Encrypted: {encrypted}")


def print_result(result: Dict[str, Any]):
    """打印API结果"""
    print(f"\n{'='*60}")
    print("API Result")
    print(f"{'='*60}")
    print(json.dumps(result, indent=2, ensure_ascii=False))


def add_credential_args(parser):
    """添加凭证参数（可选，优先使用环境变量）"""
    parser.add_argument('--product-id', help='产品ID (可选项，优先使用环境变量)')
    parser.add_argument('--access-key', help='访问密钥 (可选项，优先使用环境变量)')
    parser.add_argument('--secret-key', help='密钥 (可选项，优先使用环境变量)')


def main():
    """命令行主函数"""
    parser = argparse.ArgumentParser(
        description='大华云开放平台 - 基础通用套件客户端',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 使用环境变量（推荐）
  export DAHUA_CLOUD_PRODUCT_ID='xxx'
  export DAHUA_CLOUD_AK='xxx'
  export DAHUA_CLOUD_SK='xxx'
  python dahua_iot_client.py add -d AA08904PHA88879 -p admin123

  # 直接传递凭证
  python dahua_iot_client.py add -d AA08904PHA88879 -p admin123 --product-id xxx --access-key xxx --secret-key xxx
        """
    )
    
    # 添加全局凭证参数
    add_credential_args(parser)
    
    subparsers = parser.add_subparsers(dest='command', help='可用命令')
    
    # 添加设备
    add_parser = subparsers.add_parser('add', help='添加设备')
    add_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    add_parser.add_argument('--password', '-p', required=True, help='设备密码')
    add_parser.add_argument('--category', '-c', default='IPC', help='设备品类编码')
    add_parser.add_argument('--encrypt', '-e', default='base64', choices=['base64', 'aes256'],
                           help='加密方式（默认 base64，无需 pycryptodome）')
    add_parser.set_defaults(func=cmd_add_device)
    
    # 添加国标设备
    add_gb_parser = subparsers.add_parser('add-gb', help='添加国标设备')
    add_gb_parser.add_argument('--gb-code', '-g', required=True, help='国标码')
    add_gb_parser.add_argument('--password', '-p', required=True, help='设备注册密码')
    add_gb_parser.add_argument('--channels', '-c', type=int, required=True, help='通道总数')
    add_gb_parser.add_argument('--encrypt', '-e', default='base64', choices=['base64', 'aes256'],
                              help='密码加密方式（默认 base64）')
    add_gb_parser.add_argument('--manufacturer', '-m', help='厂商（Dahua、HIKVSION、UNKNOW）')
    add_gb_parser.add_argument('--stream-model', '-s', choices=['TCP', 'UDP'],
                              help='拉流协议（TCP 或 UDP）')
    add_gb_parser.add_argument('--device-class', '-d', choices=['NVR', 'IPC'],
                              help='设备类型（NVR 或 IPC）')
    add_gb_parser.set_defaults(func=cmd_add_gb_device)
    
    # 删除设备
    delete_parser = subparsers.add_parser('delete', help='删除设备')
    delete_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    delete_parser.set_defaults(func=cmd_delete_device)
    
    # 验证设备密码
    verify_parser = subparsers.add_parser('verify', help='验证设备密码是否正确')
    verify_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    verify_parser.add_argument('--password', '-p', required=True, help='设备密码')
    verify_parser.add_argument('--encrypt', '-e', default='base64', choices=['base64', 'aes256'],
                              help='加密方式')
    verify_parser.set_defaults(func=cmd_verify_device_password)
    
    # 查询设备列表
    list_parser = subparsers.add_parser('list', help='查询设备列表')
    list_parser.add_argument('--page', '-p', type=int, default=1, help='页码')
    list_parser.add_argument('--size', '-s', type=int, default=10, help='每页条数')
    list_parser.add_argument('--device-id', '-d', help='设备序列号（模糊查询）')
    list_parser.set_defaults(func=cmd_list_devices)
    
    # 查询设备详细信息
    details_parser = subparsers.add_parser('details', help='查询设备详细信息')
    details_parser.add_argument('device_ids', nargs='+', help='设备序列号（可多个）')
    details_parser.set_defaults(func=cmd_list_device_details)
    
    # 查询设备品类
    category_parser = subparsers.add_parser('category', help='查询设备品类')
    category_parser.add_argument('--primary', '-p', help='1级设备类别代码')
    category_parser.add_argument('--second', '-s', help='2级设备类别代码（如 IPC）')
    category_parser.add_argument('--model', '-m', help='设备型号')
    category_parser.set_defaults(func=cmd_get_category)
    
    # 查询设备在线状态
    online_parser = subparsers.add_parser('online', help='查询设备在线状态')
    online_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    online_parser.set_defaults(func=cmd_device_online)
    
    # 查询设备绑定状态
    bind_parser = subparsers.add_parser('bind', help='查询设备绑定状态')
    bind_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    bind_parser.set_defaults(func=cmd_check_device_bind)
    
    # 获取设备通道信息
    channel_parser = subparsers.add_parser('channels', help='获取设备通道信息')
    channel_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    channel_parser.set_defaults(func=cmd_get_device_channel_info)
    
    # 获取设备使能状态
    ability_parser = subparsers.add_parser('ability', help='获取设备使能状态')
    ability_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    ability_parser.add_argument('--ability-type', '-a', required=True,
                               help='使能类型，如 localRecord、motionDetect、faceCapture 等')
    ability_parser.add_argument('--channel-id', '-c', help='通道号（通道级使能需传）')
    ability_parser.set_defaults(func=cmd_get_ability_status)
    
    # 获取国标码列表
    gb_parser = subparsers.add_parser('gb-code', help='获取国标码列表')
    gb_parser.add_argument('--count', '-c', type=int, default=10, help='查询数量（1-10000，默认10）')
    gb_parser.add_argument('--prefix', '-p', help='国标码前缀（长度必须13位）')
    gb_parser.set_defaults(func=cmd_list_gb_code)
    
    # 获取国标设备注册信息
    sip_parser = subparsers.add_parser('sip-info', help='获取国标设备注册信息（Sip服务器IP和端口）')
    sip_parser.set_defaults(func=cmd_get_sip_info)
    
    # 查询国标设备详细信息
    gb_details_parser = subparsers.add_parser('gb-details', help='查询国标设备详细信息')
    gb_details_parser.add_argument('device_ids', nargs='+', help='设备序列号（可多个）')
    gb_details_parser.set_defaults(func=cmd_list_gb_device_details)
    
    # 修改国标设备信息
    modify_gb_parser = subparsers.add_parser('modify-gb', help='修改国标设备信息')
    modify_gb_parser.add_argument('--device-id', '-d', required=True, help='设备序列号')
    modify_gb_parser.add_argument('--password', '-p', help='新设备密码')
    modify_gb_parser.add_argument('--encrypt', '-e', default='base64', choices=['base64', 'aes256'],
                                 help='密码加密方式')
    modify_gb_parser.add_argument('--stream-model', '-s', choices=['TCP', 'UDP'],
                                 help='拉流协议（TCP 或 UDP）')
    modify_gb_parser.set_defaults(func=cmd_modify_gb_device)
    
    # 加密设备密码
    encrypt_parser = subparsers.add_parser('encrypt', help='加密设备密码')
    encrypt_parser.add_argument('--password', '-p', required=True, help='设备密码')
    encrypt_parser.add_argument('--secret-key', '-s', required=True, help='SecretKey')
    encrypt_parser.add_argument('--device-id', '-d', help='设备序列号')
    encrypt_parser.add_argument('--method', '-m', default='base64', choices=['base64', 'aes256'],
                               help='加密方式（默认 base64）')
    encrypt_parser.set_defaults(func=cmd_encrypt_password)
    
    args = parser.parse_args()
    
    if args.command is None:
        parser.print_help()
        return
    
    try:
        args.func(args)
    except ValueError as e:
        print(f'\n[ERROR] {e}')
        sys.exit(1)
    except Exception as e:
        print(f'\n[ERROR] Unexpected error: {e}')
        sys.exit(1)


if __name__ == "__main__":
    fix_encoding()
    main()


path: scripts/device_code_encrypt.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大华云设备密码加密工具

该脚本提供了设备密码的加密方法，支持两种加密方式：
1. Base64加密方式
2. AES256加密方式
"""

import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from typing import Optional


class DeviceCodeEncryptor:
    """设备密码加密工具类"""
    
    # AES加密的固定IV值
    IV = b'86E2DB6D77B5E9CD'
    
    # 默认密钥（当设备序列号为空时使用）
    DEFAULT_KEY = "DEFAULT_KEY"
    
    def __init__(self, secret_access_key: str, device_id: Optional[str] = None):
        """
        初始化加密工具
        
        Args:
            secret_access_key: 产品的SecretAccessKey
            device_id: 设备序列号（可选）
        """
        self.secret_access_key = secret_access_key
        self.device_id = device_id
    
    @staticmethod
    def base64_encode(text: str) -> str:
        """
        Base64编码
        
        Args:
            text: 待编码的文本
            
        Returns:
            Base64编码后的字符串
        """
        return base64.b64encode(text.encode('utf-8')).decode('utf-8')
    
    def _generate_aes_key(self) -> bytes:
        """
        生成AES密钥
        
        算法：Cut32(UpperCase(MD5-32位(UpperCase(sk))))
        
        Returns:
            32字节的AES密钥
        """
        # 如果设备序列号为空，使用默认密钥
        key_source = self.secret_access_key if self.device_id else self.DEFAULT_KEY
        
        # 计算MD5-32位
        md5_hash = hashlib.md5(key_source.upper().encode('utf-8')).hexdigest().upper()
        
        # 截取前32位
        aes_key = md5_hash[:32]
        
        return aes_key.encode('utf-8')
    
    def encrypt_base64(self, device_password: str) -> str:
        """
        方式一：Base64加密设备密码
        
        格式："Dolynk_" + Base64(设备密码)
        
        Args:
            device_password: 设备密码（明文）
            
        Returns:
            加密后的设备密码
        """
        encoded = self.base64_encode(device_password)
        return f"Dolynk_{encoded}"
    
    def encrypt_aes256(self, device_password: str) -> str:
        """
        方式二：AES256加密设备密码
        
        格式：Base64(Aes256(待加密内容, AesKey, IV初始向量))
        加密算法：Aes256/CBC/PKCS7
        
        Args:
            device_password: 设备密码（明文）
            
        Returns:
            加密后的设备密码（Base64编码）
        """
        # 生成AES密钥
        aes_key = self._generate_aes_key()
        
        # 创建AES加密器（CBC模式）
        cipher = AES.new(aes_key, AES.MODE_CBC, self.IV)
        
        # PKCS7填充并加密
        encrypted = cipher.encrypt(pad(device_password.encode('utf-8'), AES.block_size))
        
        # Base64编码
        encoded = base64.b64encode(encrypted).decode('utf-8')
        
        return encoded
    
    def encrypt_device_code(self, device_password: str, method: str = 'aes256') -> str:
        """
        加密设备密码
        
        Args:
            device_password: 设备密码（明文）
            method: 加密方式，'base64' 或 'aes256'，默认 'aes256'
            
        Returns:
            加密后的设备密码
            
        Raises:
            ValueError: 如果加密方式不支持
        """
        if method == 'base64':
            return self.encrypt_base64(device_password)
        elif method == 'aes256':
            return self.encrypt_aes256(device_password)
        else:
            raise ValueError(f"不支持的加密方式: {method}. 请使用 'base64' 或 'aes256'")


def main():
    """示例用法"""
    # 示例配置
    SECRET_ACCESS_KEY = "your_secret_access_key"
    DEVICE_ID = "your_device_id"
    DEVICE_PASSWORD = "admin123"
    
    # 创建加密工具实例
    encryptor = DeviceCodeEncryptor(SECRET_ACCESS_KEY, DEVICE_ID)
    
    print("=" * 60)
    print("设备密码加密示例")
    print("=" * 60)
    print(f"原始密码: {DEVICE_PASSWORD}")
    
    # 方式一：Base64加密
    print("\n方式一：Base64加密")
    encrypted_base64 = encryptor.encrypt_base64(DEVICE_PASSWORD)
    print(f"加密结果: {encrypted_base64}")
    
    # 方式二：AES256加密
    print("\n方式二：AES256加密")
    encrypted_aes256 = encryptor.encrypt_aes256(DEVICE_PASSWORD)
    print(f"加密结果: {encrypted_aes256}")
    
    # 使用encrypt_device_code方法（推荐）
    print("\n使用encrypt_device_code方法（推荐）")
    encrypted = encryptor.encrypt_device_code(DEVICE_PASSWORD, method='aes256')
    print(f"加密结果: {encrypted}")


if __name__ == "__main__":
    main()


path: scripts/requirements.txt
content: # 大华云开放平台 - 基础通用套件 依赖
# Python 3.8+

# HTTP请求库
requests>=2.31.0

# 加密库（用于设备密码AES256加密）
pycryptodome>=3.15.0


path: scripts/sign_helper.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大华云开发者平台API签名工具

该脚本提供了生成大华云API签名的方法，支持授权API和业务API的签名生成。
"""

import hmac
import hashlib
import time
import uuid
import json
from typing import Dict, Optional


class DahuaAPISigner:
    """大华云API签名工具类"""
    
    def __init__(self, access_key: str, secret_access_key: str, product_id: str):
        """
        初始化签名工具
        
        Args:
            access_key: 产品的AccessKey
            secret_access_key: 产品的SecretAccessKey
            product_id: 产品ID
        """
        self.access_key = access_key
        self.secret_access_key = secret_access_key
        self.product_id = product_id
    
    def generate_nonce(self) -> str:
        """生成随机数（Nonce）"""
        return str(uuid.uuid4()).replace('-', '')
    
    def generate_trace_id(self) -> str:
        """生成链路跟踪ID"""
        return f"tid-{int(time.time() * 1000)}"
    
    def generate_timestamp(self) -> str:
        """生成13位标准时间戳"""
        return str(int(time.time() * 1000))
    
    def sign_for_auth_api(self) -> Dict[str, str]:
        """
        生成授权API（获取AppAccessToken）的请求头
        
        签名算法：
        strAuthFactor = AccessKey + Timestamp + Nonce
        sign = HMAC-SHA512(strAuthFactor, secretAccessKey).toUpperCase()
        
        Returns:
            包含所有必需请求头的字典
        """
        timestamp = self.generate_timestamp()
        nonce = self.generate_nonce()
        
        # 拼接签名字符串
        str_auth_factor = self.access_key + timestamp + nonce
        
        # 生成签名
        sign = hmac.new(
            self.secret_access_key.encode('utf-8'),
            str_auth_factor.encode('utf-8'),
            hashlib.sha512
        ).hexdigest().upper()
        
        return {
            'Content-Type': 'application/json',
            'Accept-Language': 'zh-CN',
            'Version': 'v1',
            'AccessKey': self.access_key,
            'Timestamp': timestamp,
            'Nonce': nonce,
            'Sign': sign,
            'X-TraceId-Header': self.generate_trace_id(),
            'ProductID': self.product_id
        }
    
    def sign_for_business_api(self, app_access_token: str, sign_type: str = 'simple') -> Dict[str, str]:
        """
        生成业务API的请求头
        
        签名算法：
        strAuthFactor = AccessKey + AppAccessToken + Timestamp + Nonce
        sign = HMAC-SHA512(strAuthFactor, secretAccessKey).toUpperCase()
        
        Args:
            app_access_token: 应用访问令牌
            sign_type: 签名类型，'simple'表示简化签名模式
            
        Returns:
            包含所有必需请求头的字典
        """
        timestamp = self.generate_timestamp()
        nonce = self.generate_nonce()
        
        # 拼接签名字符串
        str_auth_factor = self.access_key + app_access_token + timestamp + nonce
        
        # 生成签名
        sign = hmac.new(
            self.secret_access_key.encode('utf-8'),
            str_auth_factor.encode('utf-8'),
            hashlib.sha512
        ).hexdigest().upper()
        
        headers = {
            'Content-Type': 'application/json',
            'Accept-Language': 'zh-CN',
            'Version': 'v1',
            'AccessKey': self.access_key,
            'AppAccessToken': app_access_token,
            'Timestamp': timestamp,
            'Nonce': nonce,
            'Sign': sign,
            'X-TraceId-Header': self.generate_trace_id(),
            'ProductID': self.product_id
        }
        
        # 如果使用简化签名模式，添加Sign-Type头
        if sign_type == 'simple':
            headers['Sign-Type'] = 'simple'
        
        return headers
    
    def print_auth_headers(self):
        """打印授权API请求头（用于调试）"""
        headers = self.sign_for_auth_api()
        print("授权API请求头：")
        print(json.dumps(headers, indent=2, ensure_ascii=False))
    
    def print_business_headers(self, app_access_token: str):
        """打印业务API请求头（用于调试）"""
        headers = self.sign_for_business_api(app_access_token)
        print("业务API请求头：")
        print(json.dumps(headers, indent=2, ensure_ascii=False))


def main():
    """示例用法"""
    # 示例配置（请替换为实际的AccessKey、SecretAccessKey和ProductID）
    ACCESS_KEY = "your_access_key"
    SECRET_ACCESS_KEY = "your_secret_access_key"
    PRODUCT_ID = "your_product_id"
    
    # 创建签名工具实例
    signer = DahuaAPISigner(ACCESS_KEY, SECRET_ACCESS_KEY, PRODUCT_ID)
    
    # 示例1：生成授权API请求头
    print("=" * 60)
    print("示例1：授权API请求头")
    print("=" * 60)
    auth_headers = signer.sign_for_auth_api()
    print(json.dumps(auth_headers, indent=2, ensure_ascii=False))
    
    # 示例2：生成业务API请求头
    print("\n" + "=" * 60)
    print("示例2：业务API请求头")
    print("=" * 60)
    # 假设已经获取到了AppAccessToken
    APP_ACCESS_TOKEN = "your_app_access_token"
    business_headers = signer.sign_for_business_api(APP_ACCESS_TOKEN)
    print(json.dumps(business_headers, indent=2, ensure_ascii=False))


if __name__ == "__main__":
    main()


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