first commit

This commit is contained in:
2026-02-02 11:47:53 +08:00
commit cc59e8b8da
35 changed files with 6733 additions and 0 deletions

Binary file not shown.

0
appium_automation.log Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

88
globals/alarm.py Normal file
View File

@@ -0,0 +1,88 @@
import subprocess
import os
import logging
def push_audio_to_phone(device_id, local_audio_path, phone_audio_path): # 移除默认值
"""
将本地音频文件推送到手机指定路径
"""
# 检查本地文件是否存在
if not os.path.exists(local_audio_path):
logging.error(f"本地音频文件不存在: {local_audio_path}")
return False
# 执行ADB推送命令
cmd = ["adb", "-s", device_id, "push", local_audio_path, phone_audio_path]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
logging.info(f"音频文件已推送到手机: {phone_audio_path}")
return True
else:
logging.error(f"推送音频文件失败: {result.stderr.strip()}")
return False
except subprocess.TimeoutExpired:
logging.error("推送音频文件超时")
return False
def play_audio_on_phone(device_id, phone_audio_path):
"""
控制手机播放指定路径的音频文件
"""
# 检查文件是否存在于手机
check_cmd = ["adb", "-s", device_id, "shell", "ls", phone_audio_path]
check_result = subprocess.run(check_cmd, capture_output=True, text=True)
if check_result.returncode != 0:
logging.error(f"音频文件在手机上不存在: {phone_audio_path}")
return False
# 使用am命令播放音频
cmd = [
"adb", "-s", device_id, "shell",
"am", "start",
"-a", "android.intent.action.VIEW",
"-t", "audio/*",
"-d", f"file://{phone_audio_path}"
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
logging.info(f"已成功触发手机播放音频: {phone_audio_path}")
return True
else:
logging.error(f"播放音频失败: {result.stderr.strip()}")
return False
except subprocess.TimeoutExpired:
logging.error("播放音频命令超时")
return False
def play_system_alarm(device_id):
"""
播放系统内置的警报声
"""
try:
# 方法1使用系统铃声URI
cmd = [
"adb", "-s", device_id, "shell",
"am", "start",
"-a", "android.intent.action.VIEW",
"-d", "content://settings/system/alarm_alert"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
logging.info("成功触发系统警报声")
return True
else:
logging.error(f"播放系统警报失败: {result.stderr.strip()}")
return False
except Exception as e:
logging.error(f"播放系统警报时出错: {str(e)}")
return False

479
globals/apis.py Normal file
View File

@@ -0,0 +1,479 @@
import requests
import json
import logging
import socket
from typing import Optional, Dict, Any
import globals.global_variable as global_variable
def send_tcp_command(command="StartMultiple", host="127.0.0.1", port=8888, timeout=10):
"""
使用TCP协议发送命令到指定地址和端口
参数:
command: 要发送的命令字符串(默认:"StartMultiple"
host: 目标主机地址(默认:"127.0.0.1"
port: 目标端口默认8888
timeout: 连接超时时间默认10
返回:
成功返回服务器响应字符串失败返回None
"""
# 创建TCP套接字
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
# 设置超时时间
sock.settimeout(timeout)
# 连接到目标服务器
sock.connect((host, port))
logging.info(f"已成功连接到 {host}:{port}")
# 发送命令注意需要根据服务器要求的编码格式发送这里用UTF-8
sock.sendall(command.encode('utf-8'))
logging.info(f"已发送命令: {command}")
# 接收服务器响应缓冲区大小1024字节可根据实际情况调整
response = sock.recv(1024)
if response:
response_str = response.decode('utf-8')
logging.info(f"收到响应: {response_str}")
return response_str
else:
logging.info("未收到服务器响应")
return None
except ConnectionRefusedError:
logging.info(f"连接被拒绝,请检查 {host}:{port} 是否开启服务")
return None
except socket.timeout:
logging.info(f"连接超时({timeout}秒)")
return None
except Exception as e:
logging.info(f"发送命令时发生错误: {str(e)}")
return None
def get_breakpoint_list():
"""
获取需要处理的断点列表
"""
# 请求参数
params = {
'user_name': global_variable.GLOBAL_USERNAME
}
# 请求地址
url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
try:
# 发送GET请求
response = requests.get(url, params=params, timeout=30)
# 检查请求是否成功
if response.status_code == 200:
result = response.json()
# 检查接口返回状态
if result.get('code') == 0:
data = result.get('data', [])
logging.info("成功获取断点列表,数据条数:", len(data))
# 打印断点信息
# for item in data:
# logging.info(f"线路编码: {item.get('line_num')}, "
# f"线路名称: {item.get('line_name')}, "
# f"状态: {item.get('status')}, "
# f"用户: {item.get('name')}")
return data
else:
logging.info(f"接口返回错误: {result.get('code')}")
return [{"id": 37,
"user_name": "wangshun",
"name": "wangshun",
"line_num": "L193588",
"line_name": "CDWZQ-2标-155号路基左线-461221-461570-155左-平原",
"status": 3
}]
else:
logging.info(f"请求失败,状态码: {response.status_code}")
return []
except requests.exceptions.RequestException as e:
logging.info(f"请求异常: {e}")
return []
except ValueError as e:
logging.info(f"JSON解析错误: {e}")
return []
def get_measurement_task():
"""
获取测量任务
返回: 如果有状态为1的数据返回任务信息否则返回None
"""
try:
url = "https://engineering.yuxindazhineng.com/index/index/getOne"
# 获取用户名
user_name = global_variable.GLOBAL_USERNAME
if not user_name:
logging.error("未设置用户名,无法获取测量任务")
return None
# 构造请求参数
data = {
"user_name": user_name
}
logging.info(f"请求参数: user_name={user_name}")
response = requests.post(url, data=data, timeout=10)
response.raise_for_status()
data = response.json()
logging.info(f"接口返回数据: {data}")
if data.get('code') == 0 and data.get('data'):
task_data = data['data']
if task_data.get('status') == 1:
logging.info(f"获取到测量任务: {task_data}")
return task_data
else:
logging.info("获取到的任务状态不为1不执行测量")
return None
else:
logging.warning("未获取到有效任务数据")
return None
except Exception as e:
logging.error(f"获取测量任务失败: {str(e)}")
return None
def get_end_with_num():
"""
根据线路编码获取测量任务
返回: 如果有状态为1的数据返回任务信息否则返回None
"""
try:
url = "https://engineering.yuxindazhineng.com/index/index/getOne3"
# 获取用户名
user_name = global_variable.GLOBAL_USERNAME
line_num = global_variable.GLOBAL_LINE_NUM
if not line_num:
logging.error("未设置线路编码,无法获取测量任务")
return None
if not user_name:
logging.error("未设置用户名,无法获取测量任务")
return None
# 构造请求参数
data = {
"user_name": user_name,
"line_num": line_num
}
# logging.info(f"请求参数: user_name={user_name}, line_num={line_num}")
response = requests.post(url, data=data, timeout=10)
response.raise_for_status()
data = response.json()
logging.info(f"接口返回数据: {data}")
if data.get('code') == 0 and data.get('data'):
task_data = data['data']
if task_data.get('status') == 3:
logging.info(f"获取到测量任务: {task_data}")
return task_data
else:
logging.info("获取到的任务状态不为3不执行测量")
return None
else:
# logging.warning("未获取到有效任务数据")
return None
except Exception as e:
logging.error(f"获取测量任务失败: {str(e)}")
return None
def get_work_conditions_by_linecode(linecode: str) -> Optional[Dict[str, Dict]]:
"""
通过线路编码获取工况信息
Args:
linecode: 线路编码,如 "L118134"
Returns:
返回字典,格式为 {point_id: {"sjName": "", "workinfoname": "", "work_type": ""}}
如果请求失败返回None
"""
url="http://www.yuxindazhineng.com:3002/api/comprehensive_data/get_settlement_by_linecode"
max_retries = 3 # 最大重试次数
retry_count = 0 # 当前重试计数
while retry_count < max_retries:
try:
# 准备请求参数
payload = {"linecode": linecode}
headers = {
'Content-Type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
logging.info(f"发送POST请求到: {url}")
logging.info(f"请求参数: {payload}")
# 发送POST请求
response = requests.post(
url,
json=payload,
headers=headers,
timeout=30
)
# 检查响应状态
if response.status_code != 200:
logging.error(f"HTTP请求失败状态码: {response.status_code}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
continue # 继续重试
# 解析响应数据
try:
result = response.json()
except json.JSONDecodeError as e:
logging.error(f"JSON解析失败: {str(e)}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
continue # 继续重试
# 检查API返回码
if result.get('code') != 0:
logging.error(f"API返回错误: {result.get('message', '未知错误')}")
return None
# 提取数据
data_list = result.get('data', [])
if not data_list:
logging.warning("未找到工况数据")
return {}
# 处理数据,提取所需字段
work_conditions = {}
for item in data_list:
point_id = item.get('aname')
if point_id:
work_conditions[point_id] = {
"sjName": item.get('sjName', ''),
"workinfoname": item.get('next_workinfo', ''),
"work_type": item.get('work_type', '')
}
logging.info(f"成功提取 {len(work_conditions)} 个测点的工况信息")
return work_conditions
except requests.exceptions.RequestException as e:
logging.error(f"网络请求异常: {str(e)}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
except json.JSONDecodeError as e:
logging.error(f"JSON解析失败: {str(e)}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
except Exception as e:
logging.error(f"获取工况信息时发生未知错误: {str(e)}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
# 达到最大重试次数仍失败
logging.error(f"已达到最大重试次数 ({max_retries} 次),请求失败")
return None
def get_user_max_variation(username: str) -> Optional[int]:
"""
调用POST接口根据用户名获取用户的max_variation信息
Args:
username: 目标用户名,如 "chzq02-02guoyu"
Returns:
成功返回用户的max_variation整数值
失败返回None
"""
# 接口基础配置
api_url = "http://www.yuxindazhineng.com:3002/api/accounts/get"
timeout = 30 # 超时时间(避免请求长时间阻塞)
# 1. 准备请求参数与头部
# 接口要求的POST参数JSON格式
payload = {"username": username}
# 请求头部指定JSON格式模拟浏览器UA避免被接口拦截
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
}
try:
# 2. 发送POST请求
response = requests.post(
url=api_url,
json=payload, # 自动将字典转为JSON字符串无需手动json.dumps()
headers=headers,
timeout=timeout
)
# 3. 检查HTTP响应状态200表示请求成功到达服务器
response.raise_for_status() # 若状态码非200如404、500直接抛出HTTPError
logging.info(f"接口请求成功HTTP状态码{response.status_code}")
# 4. 解析JSON响应处理文档中提到的"网页解析失败"风险)
try:
response_data = response.json()
except json.JSONDecodeError as e:
logging.error(f"接口返回数据非JSON格式解析失败{str(e)}")
logging.error(f"接口原始返回内容:{response.text[:500]}") # 打印前500字符便于排查
return None
# 5. 检查接口业务逻辑是否成功(按需求中"code=0表示查询成功"
if response_data.get("code") != 0:
logging.error(f"接口查询失败,业务错误信息:{response_data.get('message', '未知错误')}")
return None
# 6. 验证返回数据结构并提取max_variation
data_list = response_data.get("data", [])
if not data_list:
logging.warning(f"查询到用户名 {username},但未返回账号数据")
return None
# 检查第一条数据是否包含max_variation
first_user = data_list[0]
if "max_variation" not in first_user:
logging.warning(f"用户 {username} 的返回数据中缺少 max_variation 字段")
return None
max_variation = first_user["max_variation"]
logging.info(f"成功查询到用户 {username} 的 max_variation{max_variation}")
# 7. 直接返回max_variation的值
return max_variation
# 处理请求过程中的异常(网络问题、超时等)
except requests.exceptions.RequestException as e:
logging.error(f"接口请求异常(网络/超时/服务器不可达):{str(e)}")
# 若为连接错误,提示检查文档中提到的"不支持的网页类型"或域名有效性
if "ConnectionRefusedError" in str(e) or "Failed to establish a new connection" in str(e):
logging.error(f"建议排查1. 接口域名 {api_url} 是否可访问2. 服务器是否正常运行3. 端口3002是否开放")
return None
# 处理其他未知异常
except Exception as e:
logging.error(f"获取用户 {username} 的 max_variation 时发生未知错误:{str(e)}")
return None
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
def get_line_info_and_save_global(user_name: str) -> bool:
"""
调用get_name_all接口提取status=3的line_num和line_name存入全局字典
:param user_name: 接口请求参数,如"wangshun"
:return: 执行成功返回True失败/异常返回False
"""
# 接口基础配置
api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
request_params = {"user_name": user_name} # GET请求参数
timeout = 10 # 请求超时时间(秒),避免卡进程
try:
# 1. 发送GET请求
response = requests.get(
url=api_url,
params=request_params, # GET参数用params传递自动拼接到URL后规范且防乱码
timeout=timeout,
verify=False # 禁用SSL验证适配HTTPS接口
)
# 2. 校验HTTP状态码先确保请求本身成功
if response.status_code != 200:
logging.error(f"接口请求失败HTTP状态码异常{response.status_code},响应内容:{response.text}")
return False
# 3. 解析JSON响应接口返回是JSON格式需解析为字典
try:
response_data = response.json()
except Exception as e:
logging.error(f"接口返回内容非合法JSON无法解析{response.text},错误:{str(e)}")
return False
# 4. 校验业务状态码接口约定code=0成功-1失败
business_code = response_data.get("code")
if business_code == 0:
logging.info("接口业务请求成功,开始解析数据")
elif business_code == -1:
logging.error(f"接口业务请求失败业务状态码code=-1返回数据{response_data}")
return False
else:
logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档")
return False
# 5. 提取data字段校验数据是否存在
api_data_list = response_data.get("data")
if not api_data_list:
logging.warning("接口业务成功但data字段为空或无数据")
return False
# 6. 校验data是否为列表类型
if not isinstance(api_data_list, list):
logging.error(f"data字段不是列表类型实际类型{type(api_data_list)},内容:{api_data_list}")
return False
found_valid_data = False
# 7. 遍历列表提取所有status=3的数据
for item in api_data_list:
# 确保每个item是字典
if not isinstance(item, dict):
logging.warning(f"列表中的元素不是字典类型,跳过:{item}")
continue
# 获取字段值
data_status = item.get("status")
line_num = item.get("line_num")
line_name = item.get("line_name")
# 校验status是否为3且目标字段非空
if data_status == 3 and line_num and line_name:
# # 存入全局字典key=line_numvalue=line_name
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name
# 存入全局字典key=line_namevalue=line_num
global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_name] = line_num
# 如果line_name不在列表中则添加
if line_name not in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST.append(line_name)
logging.info(f"找到status=3的线路信息line_num={line_num}, line_name={line_name}")
found_valid_data = True
if found_valid_data:
logging.info(f"成功提取所有status=3的线路信息当前全局字典数据{global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT}")
return True
else:
logging.warning("data列表中未找到任何status=3且字段完整的线路信息")
return False
# 捕获所有请求相关异常(超时、连接失败、网络异常等)
except requests.exceptions.Timeout:
logging.error(f"调用get_name_all接口超时超时时间{timeout}秒,请求参数:{request_params}")
return False
except requests.exceptions.ConnectionError:
logging.error(f"调用get_name_all接口连接失败检查网络或接口地址是否正确{api_url}")
return False
except Exception as e:
logging.error(f"调用get_name_all接口时发生未知异常{str(e)}", exc_info=True) # exc_info=True打印异常堆栈方便排查
return False

763
globals/driver_utils.py Normal file
View File

@@ -0,0 +1,763 @@
import logging
import time
import subprocess
import traceback
import socket
import os
import requests
from appium import webdriver
from appium.webdriver.common.appiumby import AppiumBy
from appium.webdriver.appium_service import AppiumService
from appium.options.android import UiAutomator2Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException, InvalidSessionIdException, WebDriverException
import globals.global_variable as global_variable
def init_appium_driver(device_id, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
"""
初始化Appium驱动的全局函数
参数:
device_id: 设备ID
app_package: 应用包名,默认为"com.bjjw.cjgc"
app_activity: 应用启动Activity默认为".activity.LoginActivity"
返回:
(driver, wait): (WebDriver实例, WebDriverWait实例),如果初始化失败则抛出异常
"""
logging.info(f"设备 {device_id} 开始初始化Appium驱动")
# 创建并配置Appium选项
options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = device_id
options.app_package = app_package
options.app_activity = app_activity
options.automation_name = "UiAutomator2"
options.no_reset = True
options.auto_grant_permissions = True
options.new_command_timeout = 28800
options.udid = device_id
# 增加uiautomator2服务器启动超时时间
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
# 增加连接超时设置
options.set_capability('connection_timeout', 120000) # 120秒
try:
# 连接Appium服务器
driver_url = "http://127.0.0.1:4723/wd/hub"
logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}")
driver = webdriver.Remote(driver_url, options=options)
logging.info(f"设备 {device_id} Appium服务器连接成功")
# 初始化等待对象
wait = WebDriverWait(driver, 20)
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
# 等待应用稳定
time.sleep(2)
logging.info(f"设备 {device_id} Appium驱动初始化完成")
return driver, wait
except Exception as e:
logging.error(f"设备 {device_id} 初始化驱动失败: {str(e)}")
logging.error(f"错误类型: {type(e).__name__}")
logging.error(f"错误堆栈: {traceback.format_exc()}")
# 如果驱动已创建,尝试关闭
if 'driver' in locals() and driver:
try:
driver.quit()
except:
pass
raise
def check_session_valid(driver, device_id=None):
"""
检查当前会话是否有效
参数:
driver: WebDriver实例
device_id: 设备ID可选
返回:
bool: 会话有效返回True否则返回False
"""
if device_id is None:
device_id = global_variable.GLOBAL_DEVICE_ID
device_str = f"设备 {device_id} " if device_id else ""
if not driver:
logging.debug(f"{device_str}驱动实例为空")
return False
try:
# 首先检查driver是否有session_id属性
if not hasattr(driver, 'session_id') or not driver.session_id:
logging.debug(f"{device_str}驱动缺少有效的session_id")
return False
# 尝试获取当前上下文
current_context = driver.current_context
logging.debug(f"{device_str}会话检查通过,当前上下文: {current_context}")
return True
except InvalidSessionIdException:
logging.debug(f"{device_str}会话已失效")
return False
except WebDriverException as e:
error_msg = str(e).lower()
# 明确的会话失效错误
if any(phrase in error_msg for phrase in [
"session is either terminated or not started",
"could not proxy command to the remote server",
"socket hang up",
"connection refused",
"max retries exceeded"
]):
logging.debug(f"{device_str}会话连接错误: {error_msg[:100]}")
return False
else:
logging.debug(f"{device_str}WebDriver异常但可能不是会话失效: {error_msg[:100]}")
return True
except (ConnectionError, ConnectionRefusedError, ConnectionResetError) as e:
logging.debug(f"{device_str}网络连接错误: {str(e)}")
return False
except Exception as e:
error_msg = str(e)
# 检查是否是连接相关错误
if any(phrase in error_msg.lower() for phrase in [
"10054", "10061", "connection", "connect", "refused", "urllib3"
]):
logging.debug(f"{device_str}连接相关异常: {error_msg[:100]}")
return False
else:
logging.debug(f"{device_str}检查会话时出现其他异常: {error_msg[:100]}")
return True # 对于真正的未知异常保守返回True
def reconnect_driver(device_id, old_driver=None, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
"""
重新连接Appium驱动不重新启动应用
参数:
device_id: 设备ID
old_driver: 旧的WebDriver实例可选
app_package: 应用包名
app_activity: 应用启动Activity
返回:
(driver, wait): 新的WebDriver和WebDriverWait实例
"""
# 使用传入的device_id或从全局变量获取
if not device_id:
device_id = global_variable.GLOBAL_DEVICE_ID
# 修复device_id参数类型问题并使用全局设备ID作为备用
actual_device_id = device_id
# 检查device_id是否为有效的字符串格式
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
# 尝试从old_driver获取设备ID
if old_driver and hasattr(old_driver, 'capabilities'):
capability_device_id = old_driver.capabilities.get('udid')
if capability_device_id:
actual_device_id = capability_device_id
logging.warning(f"检测到device_id参数无效已从old_driver中提取设备ID: {actual_device_id}")
# 如果仍然没有有效的设备ID使用全局变量
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
actual_device_id = global_variable.GLOBAL_DEVICE_ID
logging.warning(f"无法获取有效设备ID使用全局变量GLOBAL_DEVICE_ID: {actual_device_id}")
device_id = actual_device_id # 使用修正后的设备ID
logging.info(f"设备 {device_id} 开始重新连接驱动(不重启应用)")
# # 首先安全关闭旧驱动
# if old_driver:
# safe_quit_driver(old_driver, device_id)
max_reconnect_attempts = 3
reconnect_delay = 5 # 秒
for attempt in range(max_reconnect_attempts):
try:
logging.info(f"设备 {device_id}{attempt + 1}次尝试重新连接")
# 确保Appium服务器运行
if not ensure_appium_server_running():
logging.warning(f"设备 {device_id} Appium服务器未运行尝试启动")
time.sleep(reconnect_delay)
continue
# 创建并配置Appium选项 - 重点:设置 autoLaunch=False 不自动启动应用
options = UiAutomator2Options()
options.platform_name = "Android"
options.device_name = device_id
options.app_package = app_package
options.app_activity = app_activity
options.automation_name = "UiAutomator2"
options.no_reset = True
options.auto_grant_permissions = True
options.new_command_timeout = 3600
options.udid = device_id
# 关键设置:不自动启动应用
options.set_capability('autoLaunch', False)
options.set_capability('skipUnlock', True)
options.set_capability('skipServerInstallation', True)
options.set_capability('skipDeviceInitialization', True)
# 增加uiautomator2服务器启动超时时间
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
# 增加连接超时设置
options.set_capability('connection_timeout', 120000) # 120秒
# 连接Appium服务器
logging.info(f"设备 {device_id} 正在连接Appium服务器不启动应用")
driver = webdriver.Remote("http://localhost:4723", options=options)
logging.info(f"设备 {device_id} Appium服务器连接成功")
# 初始化等待对象
wait = WebDriverWait(driver, 20)
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
# 不启动应用,直接附加到当前运行的应用
try:
# 获取当前运行的应用
current_package = driver.current_package
logging.info(f"设备 {device_id} 当前运行的应用: {current_package}")
# 如果当前运行的不是目标应用,尝试切换到目标应用
if current_package != app_package:
logging.info(f"设备 {device_id} 当前应用不是目标应用,尝试启动目标应用")
launch_app_manually(driver, app_package, app_activity)
else:
logging.info(f"设备 {device_id} 已成功连接到运行中的目标应用")
except Exception as attach_error:
logging.warning(f"设备 {device_id} 获取当前应用信息失败: {str(attach_error)}")
# 即使获取当前应用失败,也继续使用连接
# 验证新会话是否有效
if check_session_valid(driver, device_id):
logging.info(f"设备 {device_id} 重新连接成功")
return driver, wait
else:
logging.warning(f"设备 {device_id} 新创建的会话无效,将重试")
safe_quit_driver(driver, device_id)
except Exception as e:
logging.error(f"设备 {device_id}{attempt + 1}次重新连接失败: {str(e)}")
if attempt < max_reconnect_attempts - 1:
wait_time = reconnect_delay * (attempt + 1)
logging.info(f"设备 {device_id} 将在{wait_time}秒后重试重新连接")
time.sleep(wait_time)
else:
logging.error(f"设备 {device_id} 所有重新连接尝试均失败")
# 所有尝试都失败后才关闭旧驱动
if old_driver:
safe_quit_driver(old_driver, device_id)
raise
# 所有尝试都失败
raise Exception(f"设备 {device_id} 重新连接失败,已尝试{max_reconnect_attempts}")
# def ensure_appium_server_running(port=4723):
# """使用完整的环境变量启动Appium解决路径缺失问题"""
# try:
# # 获取当前用户的环境变量
# env = os.environ.copy()
# # ========== 仅修改这部分Windows下Node.js/Appium常见路径 ==========
# additional_paths = [
# # Windows默认Node.js安装路径64位
# os.path.join("C:\\", "Program Files\\nodejs"),
# # Windows32位Node.js路径
# os.path.join("C:\\", "Program Files (x86)\\nodejs"),
# # npm全局安装路径Windows核心appium一般装在这里
# os.path.expanduser("~\\AppData\\Roaming\\npm"),
# # 系统默认路径(防止基础命令缺失)
# os.path.join("C:\\", "Windows\\System32"),
# os.path.join("C:\\", "Windows\\System"),
# # 自定义Node.js/npm路径可选根据你的实际安装路径加
# # "D:\\Program Files\\nodejs", # 若你装在D盘解开注释并修改
# ]
# # ========== Windows路径修改结束 ==========
# # 更新PATH环境变量跨系统通用os.pathsep自动适配Windows的;和macOS的:
# current_path = env.get('PATH', '')
# new_path = current_path + os.pathsep + os.pathsep.join(additional_paths)
# env['PATH'] = new_path
# # 构建启动命令(和原函数一致,无修改)
# appium_cmd = f"appium -p {port} --log-level error"
# # 使用完整环境启动跨系统通用Windows下正常执行
# process = subprocess.Popen(
# appium_cmd,
# shell=True, # Windows下字符串命令必须开启和macOS一致
# env=env, # 传入补全后的Windows环境变量核心
# stdout=subprocess.PIPE, # 捕获输出,控制台不刷屏
# stderr=subprocess.PIPE,
# text=True # 输出为字符串,无需手动解码(跨系统通用)
# )
# logging.info(f"Appium启动进程已创建PID: {process.pid}")
# # 等待并校验服务启动成功需确保wait_for_appium_start已定义timeout有值
# return wait_for_appium_start(port)
# except Exception as e:
# logging.error(f"Windows下启动Appium时出错: {str(e)}")
# return False
def is_port_in_use(port):
"""检查端口是否被占用"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(('127.0.0.1', port)) == 0
def kill_system_process(process_name):
"""杀掉系统进程"""
try:
# Windows
subprocess.run(f"taskkill /F /IM {process_name}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
except Exception:
pass
def start_appium_server(port=4723):
"""启动 Appium 服务,强制指定路径兼容性"""
# 1. 先尝试清理可能占用的 node 进程
if is_port_in_use(port):
logging.warning(f"端口 {port} 被占用,尝试清理 node.exe...")
kill_system_process("node.exe")
time.sleep(2)
# 2. 构造启动命令
# 注意:这里增加了 --base-path /wd/hub 解决 404 问题
# --allow-cors 允许跨域,有时候能解决连接问题
appium_cmd = f"appium -p {port} --base-path /wd/hub --allow-cors"
logging.info(f"正在启动 Appium: {appium_cmd}")
try:
# 使用 shell=True 在 Windows 上更稳定
# creationflags=subprocess.CREATE_NEW_CONSOLE 可以让它在后台运行不弹出窗口
subprocess.Popen(appium_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
logging.info("Appium 启动命令已发送,等待服务就绪...")
except Exception as e:
logging.error(f"启动 Appium 进程失败: {e}")
def check_server_status(port):
"""检测服务器状态,兼容 Appium 1.x 和 2.x 路径"""
base_url = f"http://127.0.0.1:{port}"
check_paths = ["/wd/hub/status", "/status"] # 优先检查 /wd/hub
for path in check_paths:
try:
url = f"{base_url}{path}"
response = requests.get(url, timeout=1)
if response.status_code == 200:
return True
except:
pass
return False
def ensure_appium_server_running(port=4723):
"""确保 Appium 服务器正在运行,如果没运行则启动它"""
# 1. 第一次快速检测
if check_server_status(port):
logging.info(f"Appium 服务已在端口 {port} 运行")
return True
# 2. 如果没运行,启动它
logging.warning(f"Appium 未在端口 {port} 运行,准备启动...")
start_appium_server(port)
# 3. 循环等待启动成功(最多等待 20 秒)
max_retries = 20
for i in range(max_retries):
if check_server_status(port):
logging.info("Appium 服务启动成功并已就绪!")
return True
time.sleep(1)
if i % 5 == 0:
logging.info(f"等待 Appium 启动中... ({i}/{max_retries})")
logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。")
return False
# ... (保留 init_appium_driver, safe_quit_driver 等其他函数) ...
def wait_for_appium_start(port, timeout=10):
"""
检测指定端口的Appium服务是否真正启动并可用
:param port: Appium服务端口
:param timeout: 最大等待时间(秒)
:return: 服务就绪返回True超时/失败返回False
"""
# Appium官方状态查询接口
check_url = f"http://localhost:{port}/wd/hub/status"
# 检测开始时间
start_check_time = time.time()
logging.info(f"开始检测Appium服务是否就绪端口{port},最大等待{timeout}")
while time.time() - start_check_time < timeout:
try:
# 发送HTTP请求超时1秒避免单次检测卡太久
response = requests.get(
url=check_url,
timeout=1,
verify=False # 本地接口禁用SSL验证
)
# 接口返回200HTTP成功状态码且JSON中status=0Appium服务正常
if response.status_code == 200 and response.json().get("status") == 0:
logging.info(f"Appium服务检测成功端口{port}已就绪")
return True
except Exception as e:
# 捕获所有异常连接拒绝、超时、JSON解析失败等说明服务未就绪
logging.debug(f"本次检测Appium服务未就绪{str(e)}") # 调试日志,不刷屏
# 检测失败休眠1秒后重试
time.sleep(1)
# 循环结束→超时
logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪")
return False
def safe_quit_driver(driver, device_id=None):
"""
安全关闭驱动的全局函数
参数:
driver: WebDriver实例
device_id: 设备ID可选
"""
if device_id is None:
device_id = global_variable.GLOBAL_DEVICE_ID
device_str = f"设备 {device_id} " if device_id else ""
logging.info(f"{device_str}开始关闭驱动")
if not driver:
logging.info(f"{device_str}没有可关闭的驱动实例")
return
# 检查driver是否为WebDriver实例或是否有quit方法
if not hasattr(driver, 'quit'):
logging.warning(f"{device_str}驱动对象类型无效不具有quit方法: {type(driver).__name__}")
return
max_quit_attempts = 3
for attempt in range(max_quit_attempts):
try:
logging.info(f"{device_str}尝试关闭驱动 (尝试 {attempt + 1}/{max_quit_attempts})")
driver.quit()
logging.info(f"{device_str}驱动已成功关闭")
return
except Exception as e:
logging.error(f"{device_str}关闭驱动时出错 (尝试 {attempt + 1}/{max_quit_attempts}): {str(e)}")
if attempt < max_quit_attempts - 1:
# 等待一段时间后重试
wait_time = 2
logging.info(f"{device_str}将在 {wait_time} 秒后重试")
time.sleep(wait_time)
else:
logging.critical(f"{device_str}尝试多次关闭驱动失败,可能导致资源泄漏")
def is_app_launched(driver, package_name="com.bjjw.cjgc"):
"""
检查应用是否已启动
参数:
driver: WebDriver实例
package_name: 应用包名,默认为"com.bjjw.cjgc"
返回:
bool: 如果应用已启动则返回True否则返回False
"""
try:
# 通过检查当前活动的包名来确认应用是否已启动
current_package = driver.current_package
return current_package == package_name
except Exception as e:
logging.error(f"检查应用启动状态时出错: {str(e)}")
return False
def launch_app_manually(driver, device_id, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"):
"""
手动启动应用
参数:
driver: WebDriver实例
package_name: 应用包名,默认为"com.bjjw.cjgc"
activity: 应用启动Activity默认为".activity.LoginActivity"
"""
try:
if not device_id:
device_id = global_variable.GLOBAL_DEVICE_ID
# 尝试从driver获取设备ID
if driver and hasattr(driver, 'capabilities'):
device_id = driver.capabilities.get('udid')
device_str = f"设备 {device_id} " if device_id else ""
else:
device_str = ""
logging.info(f"{device_str}尝试手动启动应用: {package_name}/{activity}")
# 首先使用ADB命令退出应用
if device_id:
try:
# 使用ADB命令强制停止应用
stop_cmd = [
"adb", "-s", device_id,
"shell", "am", "force-stop",
package_name
]
stop_result = subprocess.run(stop_cmd, capture_output=True, text=True, timeout=15)
if stop_result.returncode == 0:
logging.info(f"{device_str}已使用ADB命令成功退出应用")
else:
logging.warning(f"{device_str}ADB退出应用失败: {stop_result.stderr}")
except Exception as stop_error:
logging.warning(f"{device_str}退出应用时出错: {str(stop_error)}")
# 首先尝试使用driver的execute_script方法启动应用
try:
if driver:
driver.execute_script("mobile: startActivity", {
"intent": f"{package_name}/{activity}"
})
logging.info(f"{device_str}已使用Appium startActivity命令启动应用")
except Exception as inner_e:
logging.warning(f"{device_str}使用Appium startActivity命令失败: {str(inner_e)}尝试使用ADB命令")
# 如果device_id可用使用ADB命令启动应用
if device_id:
cmd = [
"adb", "-s", device_id,
"shell", "am", "start",
"-n", f"{package_name}/{activity}"
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
if result.returncode == 0:
logging.info(f"{device_str}已使用ADB命令成功启动应用")
else:
logging.error(f"{device_str}ADB启动应用失败: {result.stderr}")
else:
logging.warning("无法获取设备ID无法使用ADB命令启动应用")
# 设置屏幕永不休眠
if device_id:
try:
# 使用ADB命令设置屏幕永不休眠
screen_timeout_cmd = [
"adb", "-s", device_id,
"shell", "settings", "put", "system", "screen_off_timeout", "0"
]
timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15)
if timeout_result.returncode == 0:
logging.info(f"{device_str}已成功设置屏幕永不休眠")
else:
logging.warning(f"{device_str}设置屏幕永不休眠失败: {timeout_result.stderr}")
except Exception as timeout_error:
logging.warning(f"{device_str}设置屏幕永不休眠时出错: {str(timeout_error)}")
# 等待应用启动
time.sleep(5)
except Exception as e:
logging.error(f"手动启动应用时出错: {str(e)}")
logging.error(f"错误堆栈: {traceback.format_exc()}")
def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3):
"""
跳转到主页面并点击对应的导航菜单按钮(带重试机制)
参数:
driver: WebDriver实例
device_id: 设备ID
tabber_button_text: 导航菜单按钮的文本
max_retries: 最大重试次数
返回:
bool: 成功返回True失败返回False
"""
retry_count = 0
while retry_count < max_retries:
try:
logging.info(f"设备 {device_id}{retry_count + 1} 次尝试执行导航操作")
if not check_session_valid(driver, device_id):
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
try:
# 重新连接获取新的driver
new_driver, _ = reconnect_driver(device_id, driver)
driver = new_driver # 更新driver引用
logging.info(f"设备 {device_id} 驱动重连成功")
except Exception as e:
logging.error(f"设备 {device_id} 驱动重连失败: {str(e)}")
retry_count += 1
if retry_count < max_retries:
time.sleep(2)
continue
else:
return False
# 检查当前是否已经在主页面
current_activity = driver.current_activity
logging.info(f"设备 {device_id} 当前Activity: {current_activity}")
if ".activity.MainActivity" in current_activity:
logging.info(f"设备 {device_id} 已在主页面")
else:
logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面")
max_back_presses = 10 # 最大返回键次数
back_press_count = 0
while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses:
try:
if not check_session_valid(driver, device_id):
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
if not reconnect_driver(device_id, driver):
logging.error(f"设备 {device_id} 驱动重连失败")
# 点击返回按钮
driver.back()
back_press_count += 1
time.sleep(1)
# 更新当前Activity
current_activity = driver.current_activity
logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后当前Activity: {current_activity}")
except Exception as inner_e:
logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}")
break
# 检查是否成功回到主页面
if ".activity.MainActivity" not in current_activity:
logging.warning(f"设备 {device_id} 无法回到主页面当前Activity: {current_activity}")
# 不立即返回,继续重试逻辑
retry_count += 1
if retry_count < max_retries:
logging.info(f"设备 {device_id} 等待2秒后重试...")
time.sleep(2)
continue
else:
logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面")
return False
# 现在已经在主页面,点击指定的导航菜单按钮
# logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}")
try:
tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text)
# 点击按钮
tabber_button.click()
logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}")
# 等待页面加载
time.sleep(2)
# 验证操作是否成功
# 可以添加一些验证逻辑,比如检查是否跳转到目标页面
new_activity = driver.current_activity
logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}")
return True
except TimeoutException:
logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时")
except Exception as e:
logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}")
# 检查会话有效性并尝试重连
if not check_session_valid(driver, device_id):
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
if reconnect_driver(device_id, driver):
logging.info(f"设备 {device_id} 驱动重连成功,继续重试")
# 重连成功后继续循环
retry_count += 1
if retry_count < max_retries:
time.sleep(2)
continue
else:
logging.error(f"设备 {device_id} 驱动重连失败")
return False
else:
# 会话有效但点击失败,可能是页面元素问题
logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题")
# 如果点击按钮失败,增加重试计数
retry_count += 1
if retry_count < max_retries:
logging.info(f"设备 {device_id} 点击按钮失败等待2秒后第 {retry_count + 1} 次重试...")
time.sleep(2)
else:
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
return False
except Exception as e:
logging.error(f"设备 {device_id}{retry_count + 1} 次尝试时出错: {str(e)}")
retry_count += 1
if retry_count < max_retries:
logging.info(f"设备 {device_id} 等待2秒后重试...")
time.sleep(2)
else:
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
return False
return False
def check_connection_error(exception):
"""检查是否为连接拒绝错误"""
error_str = str(exception)
connection_errors = [
'远程主机强迫关闭了一个现有的连接',
'由于目标计算机积极拒绝,无法连接',
'ConnectionResetError',
'NewConnectionError',
'10054',
'10061'
]
return any(error in error_str for error in connection_errors)
def restart_appium_server(port=4723):
"""重启Appium服务器"""
try:
# 杀死可能存在的Appium进程
subprocess.run(['taskkill', '/f', '/im', 'node.exe'],
capture_output=True, shell=True)
time.sleep(2)
# 启动Appium服务器
# appium_command = f'appium -p {port}'
appium_command = f"appium -p {port} --base-path /wd/hub"
subprocess.Popen(appium_command, shell=True)
# 等待Appium启动
time.sleep(10)
return True
except Exception as e:
print(f"重启Appium服务器失败: {str(e)}")
return False
def is_appium_running(port=4723):
"""检查Appium服务器是否在运行"""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
result = s.connect_ex(('127.0.0.1', port))
return result == 0
except:
return False

298
globals/ex_apis.py Normal file
View File

@@ -0,0 +1,298 @@
# external_apis.py
import requests
import json
import logging
import random
from typing import Dict, Tuple, Optional
import time
class WeatherAPI:
def __init__(self):
self.logger = logging.getLogger(__name__)
# 使用腾讯天气API
self.base_url = "https://wis.qq.com/weather/common"
def parse_city(self, city_string: str) -> Tuple[str, str, str]:
"""
解析城市字符串,返回省份、城市、区县
参数:
city_string: 完整的地址字符串
返回:
(province, city, county)
"""
# 匹配省份或自治区
province_regex = r"(.*?)(省|自治区)"
# 匹配城市或州
city_regex = r"(.*?省|.*?自治区)(.*?市|.*?州)"
# 匹配区、县或镇
county_regex = r"(.*?市|.*?州)(.*?)(区|县|镇)"
province = ""
city = ""
county = ""
import re
# 先尝试匹配省份或自治区
province_match = re.search(province_regex, city_string)
if province_match:
province = province_match.group(1).strip()
# 然后尝试匹配城市或州
city_match = re.search(city_regex, city_string)
if city_match:
city = city_match.group(2).strip()
else:
# 如果没有匹配到城市,则可能是直辖市或者直接是区/县
city = city_string
# 最后尝试匹配区、县或镇
county_match = re.search(county_regex, city_string)
if county_match:
county = county_match.group(2).strip()
# 如果有区、县或镇,那么前面的城市部分需要重新解析
if city_match:
city = city_match.group(2).strip()
# 特殊情况处理,去除重复的省市名称
if city and province and city.startswith(province):
city = city.replace(province, "").strip()
if county and city and county.startswith(city):
county = county.replace(city, "").strip()
# 去除后缀
city = city.rstrip('市州')
if county:
county = county.rstrip('区县镇')
# self.logger.info(f"解析结果 - 省份: {province}, 城市: {city}, 区县: {county}")
return province, city, county
def get_weather_by_qq_api(self, province: str, city: str, county: str) -> Optional[Dict]:
"""
使用腾讯天气API获取天气信息
参数:
province: 省份
city: 城市
county: 区县
返回:
天气信息字典 or None
"""
try:
params = {
'source': 'pc',
'weather_type': 'observe',
'province': province,
'city': city,
'county': county
}
response = requests.get(self.base_url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if data.get('status') == 200:
observe_data = data.get('data', {}).get('observe', {})
return {
'weather': observe_data.get('weather', ''),
'temperature': observe_data.get('degree', ''),
'pressure': observe_data.get('pressure', '1013')
}
else:
self.logger.error(f"腾讯天气API错误: {data.get('message')}")
return None
except Exception as e:
self.logger.error(f"腾讯天气API调用失败: {str(e)}")
return None
def normalize_weather_text(self, weather_text: str) -> str:
"""
将天气描述标准化为: 晴;阴;雨;雪;风;其他
参数:
weather_text: 原始天气描述
返回:
标准化后的天气文本
"""
if not weather_text:
return '其他'
weather_text_lower = weather_text.lower()
# 晴
if any(word in weather_text_lower for word in ['', 'sunny', 'clear']):
return ''
# 阴
elif any(word in weather_text_lower for word in ['', '多云', 'cloudy', 'overcast']):
return ''
# 雨
elif any(word in weather_text_lower for word in ['', 'rain', 'drizzle', 'shower']):
return ''
# 雪
elif any(word in weather_text_lower for word in ['', 'snow']):
return ''
# 风
elif any(word in weather_text_lower for word in ['', 'wind']):
return ''
# 其他
else:
return '其他'
def adjust_pressure(self, pressure: float) -> float:
"""
调整气压值低于700时填700-750之间随机一个高于700就按实际情况填
参数:
pressure: 原始气压值
返回:
调整后的气压值
"""
try:
pressure_float = float(pressure)
if pressure_float < 700:
adjusted_pressure = random.randint(700, 750)
self.logger.info(f"气压值 {pressure_float} 低于700调整为: {adjusted_pressure:.1f}")
return round(adjusted_pressure, 1)
else:
self.logger.info(f"使用实际气压值: {pressure_float}")
return round(pressure_float, 1)
except (ValueError, TypeError):
self.logger.warning(f"气压值格式错误: {pressure}使用默认值720")
return round(random.randint(700, 750), 1)
def get_weather_by_address(self, address: str, max_retries: int = 2) -> Optional[Dict]:
"""
根据地址获取天气信息
参数:
address: 地址字符串
max_retries: 最大重试次数
返回:
{
'weather': '晴/阴/雨/雪/风/其他',
'temperature': 温度值,
'pressure': 气压值
} or None
"""
# self.logger.info(f"开始获取地址 '{address}' 的天气信息")
# 首先解析地址
province, city, county = self.parse_city(address)
if not province and not city:
self.logger.error("无法解析地址")
return self.get_fallback_weather()
# 获取天气信息
weather_data = None
for attempt in range(max_retries):
try:
# self.logger.info(f"尝试获取天气信息 (第{attempt + 1}次)")
weather_data = self.get_weather_by_qq_api(province, city, county)
if weather_data:
break
time.sleep(1) # 短暂延迟后重试
except Exception as e:
self.logger.warning(f"{attempt + 1}次尝试失败: {str(e)}")
time.sleep(1)
if not weather_data:
self.logger.warning("获取天气信息失败,使用备用数据")
return self.get_fallback_weather()
# 处理天气数据
try:
# 标准化天气文本
normalized_weather = self.normalize_weather_text(weather_data['weather'])
# 调整气压值
adjusted_pressure = self.adjust_pressure(weather_data['pressure'])
# 处理温度
temperature = float(weather_data['temperature'])
result = {
'weather': normalized_weather,
'temperature': round(temperature, 1),
'pressure': adjusted_pressure
}
self.logger.info(f"成功获取天气信息: {result}")
return result
except Exception as e:
self.logger.error(f"处理天气数据时出错: {str(e)}")
return self.get_fallback_weather()
def get_fallback_weather(self) -> Dict:
"""
获取备用天气数据当所有API都失败时使用
返回:
默认天气数据
"""
self.logger.info("使用备用天气数据")
return {
'weather': '',
'temperature': round(random.randint(15, 30), 1),
'pressure': round(random.randint(700, 750), 1)
}
def get_weather_simple(self, address: str) -> Tuple[str, float, float]:
"""
简化接口:直接返回天气、温度、气压
参数:
address: 地址字符串
返回:
(weather, temperature, pressure)
"""
weather_data = self.get_weather_by_address(address)
if weather_data:
return weather_data['weather'], weather_data['temperature'], weather_data['pressure']
else:
fallback = self.get_fallback_weather()
return fallback['weather'], fallback['temperature'], fallback['pressure']
# 创建全局实例
weather_api = WeatherAPI()
# 直接可用的函数
def get_weather_by_address(address: str) -> Optional[Dict]:
"""
根据地址获取天气信息(直接调用函数)
参数:
address: 地址字符串
返回:
{
'weather': '晴/阴/雨/雪/风/其他',
'temperature': 温度值,
'pressure': 气压值
} or None
"""
return weather_api.get_weather_by_address(address)
def get_weather_simple(address: str) -> Tuple[str, float, float]:
"""
简化接口:直接返回天气、温度、气压
参数:
address: 地址字符串
返回:
(weather, temperature, pressure)
"""
return weather_api.get_weather_simple(address)

View File

@@ -0,0 +1,17 @@
# 全局变量
GLOBAL_DEVICE_ID = "" # 设备ID
GLOBAL_USERNAME = "czyuzongwen" # 用户名
GLOBAL_CURRENT_PROJECT_NAME = "" # 当前测试项目名称
GLOBAL_LINE_NUM = "" # 线路编码
GLOBAL_BREAKPOINT_STATUS_CODES = [0,3] # 要获取的断点状态码列表
GLOBAL_UPLOAD_BREAKPOINT_LIST = []
GLOBAL_UPLOAD_BREAKPOINT_DICT = {}
GLOBAL_TESTED_BREAKPOINT_LIST = [] # 测量结束的断点列表
LINE_TIME_MAPPING_DICT = {} # 存储所有线路编码和对应的时间的全局字典
GLOBAL_BREAKPOINT_DICT = {} # 存储测量结束的断点名称和对应的线路编码的全局字典
GLOBAL_NAME_TO_ID_MAP = {} # 存储所有数据员姓名和对应的身份证号的全局字典
GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = [] # 上传成功的`断点列表

59
globals/ids.py Normal file
View File

@@ -0,0 +1,59 @@
# ids.py
# 登录界面
LOGIN_USERNAME = "com.bjjw.cjgc:id/et_user_name"
LOGIN_PASSWORD = "com.bjjw.cjgc:id/et_user_psw"
LOGIN_BTN = "com.bjjw.cjgc:id/btn_login"
# 更新相关
UPDATE_WORK_BASE = "com.bjjw.cjgc:id/btn_update_basepoint"
UPDATE_LEVEL_LINE = "com.bjjw.cjgc:id/btn_update_line"
UPDATE_LEVEL_LINE_CONFIRM = "com.bjjw.cjgc:id/commit"
# 弹窗 & 加载
ALERT_DIALOG = "android:id/content"
LOADING_DIALOG = "android:id/custom"
# 底部导航栏
DOWNLOAD_TABBAR_ID = "com.bjjw.cjgc:id/img_1_layout"
MEASURE_TABBAR_ID = "com.bjjw.cjgc:id/img_3_layout"
# 测量相关
MEASURE_BTN_ID = "com.bjjw.cjgc:id/select_point_update_tip_tv"
MEASURE_LIST_ID = "com.bjjw.cjgc:id/line_list"
MEASURE_LISTVIEW_ID = "com.bjjw.cjgc:id/itemContainer"
MEASURE_NAME_TEXT_ID = "com.bjjw.cjgc:id/title"
MEASURE_NAME_ID = "com.bjjw.cjgc:id/sectName"
MEASURE_BACK_ID = "com.bjjw.cjgc:id/btn_back"
MEASURE_BACK_ID_2 = "com.bjjw.cjgc:id/stop_measure_btn"
# 天气、观测类型
MEASURE_TITLE_ID = "com.bjjw.cjgc:id/title_bar"
MEASURE_WEATHER_ID = "com.bjjw.cjgc:id/point_list_weather_sp"
MEASURE_TYPE_ID = "com.bjjw.cjgc:id/point_list_mtype_sp"
MEASURE_SELECT_ID = "android:id/select_dialog_listview"
SELECT_DIALOG_TEXT1_ID = "android:id/text1"
MEASURE_PRESSURE_ID = "com.bjjw.cjgc:id/point_list_barometric_et"
MEASURE_TEMPERATURE_ID = "com.bjjw.cjgc:id/point_list_temperature_et"
MEASURE_SAVE_ID = "com.bjjw.cjgc:id/select_point_order_save_btn"
# 日期选择器
DATE_START = "com.bjjw.cjgc:id/date"
DATE_END = "com.bjjw.cjgc:id/date_end"
SCRCOLL_YEAR = "com.bjjw.cjgc:id/wheelView1"
SCRCOLL_MONTH = "com.bjjw.cjgc:id/wheelView2"
SCRCOLL_DAY = "com.bjjw.cjgc:id/wheelView3"
SCRCOLL_CONFIRM = "com.bjjw.cjgc:id/okBtn"
SCRCOLL_CANCEL = "com.bjjw.cjgc:id/cancelBtn"
# 其他
CONNECT_LEVEL_METER = "com.bjjw.cjgc:id/point_conn_level_btn"
PINGCHAR_PROCESS = "com.bjjw.cjgc:id/point_measure_btn"
LEVEL_METER_MANAGER = "com.bjjw.cjgc:id/btn_back"
LEVEL_METER_MANAGER_LIST = "com.bjjw.cjgc:id/total_station_expandlist"
START_MEASURE = "com.bjjw.cjgc:id/btn_control_begin_or_end"
ALTER_MEASURE = "com.bjjw.cjgc:id/order_title"
MATCH_VISIABLE = "com.bjjw.cjgc:id/title_paired_devices"
BLUETOOTH_PAIR = "com.bjjw.cjgc:id/paired_devices"
REPID_MEASURE = "com.bjjw.cjgc:id/measure_remeasure_all_btn"
ONE = "com.bjjw.cjgc:id/auto_measure_all_station_text"

478
main.py Normal file
View File

@@ -0,0 +1,478 @@
# actions.py 主自动化脚本
import os
import logging
import time
import subprocess
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import globals.ids as ids
import globals.global_variable as global_variable # 导入全局变量模块
import permissions # 导入权限处理模块
import globals.apis as apis
from globals.driver_utils import init_appium_driver, ensure_appium_server_running, safe_quit_driver, is_app_launched, launch_app_manually
from page_objects.login_page import LoginPage
from page_objects.download_tabbar_page import DownloadTabbarPage
from page_objects.screenshot_page import ScreenshotPage
from page_objects.upload_config_page import UploadConfigPage
from page_objects.more_download_page import MoreDownloadPage
# 配置日志
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s: %(message)s",
handlers=[
logging.FileHandler("appium_automation.log"),
logging.StreamHandler()
]
)
class DeviceAutomation:
@staticmethod
def get_device_id() -> str:
# """
# 获取设备ID优先使用已连接设备否则使用全局配置
# """
# try:
# # 检查已连接设备
# result = subprocess.run(
# ["adb", "devices"],
# capture_output=True,
# text=True,
# timeout=10
# )
# # 解析设备列表
# for line in result.stdout.strip().split('\n')[1:]:
# if line.strip() and "device" in line and "offline" not in line:
# device_id = line.split('\t')[0]
# logging.info(f"使用已连接设备: {device_id}")
# global_variable.GLOBAL_DEVICE_ID = device_id
# return device_id
# except Exception as e:
# logging.warning(f"设备检测失败: {e}")
"""
获取设备ID优先使用无线连接设备否则尝试开启无线调试最后使用全局配置
"""
try:
# 检查已连接设备
result = subprocess.run(
["adb", "devices"],
capture_output=True,
text=True,
timeout=10
)
# 解析设备列表
wireless_device_id = None
usb_device_id = None
# 先查找无线连接的设备IP:端口格式)
for line in result.stdout.strip().split('\n')[1:]:
if line.strip() and "device" in line and "offline" not in line:
current_device = line.split('\t')[0]
# 检查是否为IP:端口格式的无线连接
if ":" in current_device and any(char.isdigit() for char in current_device):
wireless_device_id = current_device
logging.info(f"使用无线连接设备: {wireless_device_id}")
global_variable.GLOBAL_DEVICE_ID = wireless_device_id
return wireless_device_id
else:
# 记录第一个USB连接的设备
if not usb_device_id:
usb_device_id = current_device
# 如果没有找到无线连接的设备尝试使用USB设备开启无线调试
if not wireless_device_id and usb_device_id:
logging.info(f"未找到无线连接设备尝试使用USB设备 {usb_device_id} 开启无线调试")
# 尝试获取设备IP地址
try:
import re
import time
ip_result = subprocess.run(
["adb", "-s", usb_device_id, "shell", "ip", "-f", "inet", "addr", "show", "wlan0"],
capture_output=True,
text=True,
timeout=10
)
# 解析IP地址
ip_output = ip_result.stdout
if "inet " in ip_output:
# 提取IP地址
ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', ip_output)
if ip_match:
device_ip = ip_match.group(1)
logging.info(f"获取到设备IP地址: {device_ip}")
# 开启无线调试
tcpip_result = subprocess.run(
["adb", "-s", usb_device_id, "tcpip", "5555"],
capture_output=True,
text=True,
timeout=10
)
if "restarting in TCP mode port: 5555" in tcpip_result.stdout:
logging.info("无线调试已开启,端口: 5555")
# 等待几秒钟让设备准备好
time.sleep(3)
# 连接到无线设备
connect_result = subprocess.run(
["adb", "connect", f"{device_ip}:5555"],
capture_output=True,
text=True,
timeout=10
)
if "connected to" in connect_result.stdout:
logging.info(f"成功连接到无线设备: {device_ip}:5555")
global_variable.GLOBAL_DEVICE_ID = f"{device_ip}:5555"
return f"{device_ip}:5555"
else:
logging.warning(f"连接无线设备失败: {connect_result.stderr}")
logging.info(f"使用USB设备: {usb_device_id}")
global_variable.GLOBAL_DEVICE_ID = usb_device_id
return usb_device_id
else:
logging.warning(f"开启无线调试失败: {tcpip_result.stderr}")
logging.info(f"使用USB设备: {usb_device_id}")
global_variable.GLOBAL_DEVICE_ID = usb_device_id
return usb_device_id
else:
logging.warning("未找到设备IP地址")
logging.info(f"使用USB设备: {usb_device_id}")
global_variable.GLOBAL_DEVICE_ID = usb_device_id
return usb_device_id
else:
logging.warning("无法获取设备IP地址可能设备未连接到WiFi")
logging.info(f"使用USB设备: {usb_device_id}")
global_variable.GLOBAL_DEVICE_ID = usb_device_id
return usb_device_id
except Exception as e:
logging.warning(f"开启无线调试时出错: {str(e)}")
logging.info(f"使用USB设备: {usb_device_id}")
global_variable.GLOBAL_DEVICE_ID = usb_device_id
return usb_device_id
except Exception as e:
logging.warning(f"设备检测失败: {e}")
# 使用全局配置
device_id = global_variable.GLOBAL_DEVICE_ID
logging.info(f"使用全局配置设备: {device_id}")
return device_id
def __init__(self, device_id=None):
# 如果没有提供设备ID则自动获取
if device_id is None:
self.device_id = self.get_device_id()
else:
self.device_id = device_id
# 初始化权限
if permissions.grant_appium_permissions(self.device_id):
logging.info(f"设备 {self.device_id} 权限授予成功")
else:
logging.warning(f"设备 {self.device_id} 权限授予失败")
# 确保Appium服务器正在运行
ensure_appium_server_running(4723)
# 初始化驱动
self.init_driver()
# 先拼接,后创建测试结果目录
self.results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_results')
os.makedirs(self.results_dir, exist_ok=True)
def init_driver(self):
"""初始化Appium驱动"""
try:
# 使用全局函数初始化驱动
self.driver, self.wait = init_appium_driver(self.device_id)
# 初始化页面对象
logging.info(f"设备 {self.device_id} 开始初始化页面对象")
self.login_page = LoginPage(self.driver, self.wait)
self.download_tabbar_page = DownloadTabbarPage(self.driver, self.wait, self.device_id)
self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id)
self.upload_config_page = UploadConfigPage(self.driver, self.wait, self.device_id)
self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id)
logging.info(f"设备 {self.device_id} 所有页面对象初始化完成")
# 检查应用是否成功启动
if is_app_launched(self.driver):
logging.info(f"设备 {self.device_id} 沉降观测App已成功启动")
else:
logging.warning(f"设备 {self.device_id} 应用可能未正确启动")
# 手动启动应用
launch_app_manually(self.driver, self.device_id)
except Exception as e:
logging.error(f"设备 {self.device_id} 初始化驱动失败: {str(e)}")
raise
def is_app_launched(self):
"""检查应用是否已启动"""
try:
return is_app_launched(self.driver)
except Exception as e:
logging.error(f"设备 {self.device_id} 检查应用启动状态时出错: {str(e)}")
return False
def is_element_present(self, by, value):
"""检查元素是否存在"""
try:
self.driver.find_element(by, value)
return True
except NoSuchElementException:
return False
def handle_app_state(self):
"""根据当前应用状态处理相应的操作"""
try:
login_btn_exists = self.login_page.is_login_page()
if not login_btn_exists:
logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面")
if self.navigate_to_login_page(self.driver, self.device_id):
logging.info(f"设备 {self.device_id} 成功跳转到登录页面")
return self.handle_app_state() # 递归调用处理登录后的状态
else:
logging.error(f"设备 {self.device_id} 跳转到登录页面失败")
return False
# 处理登录页面状态
logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作")
max_retries = 1
login_success = False
for attempt in range(max_retries + 1):
if self.login_page.login():
login_success = True
break
else:
if attempt < max_retries:
logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries})")
time.sleep(2) # 等待2秒后重试
else:
logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数")
if not login_success:
return False
logging.info(f"设备 {self.device_id} 登录成功,继续执行更新操作")
time.sleep(1)
# 执行更新操作
if not self.download_tabbar_page.download_tabbar_page_manager():
logging.error(f"设备 {self.device_id} 更新操作执行失败")
return False
# 获取状态为3的线路。
apis.get_line_info_and_save_global(user_name=global_variable.GLOBAL_USERNAME);
# # 虚拟数据替代
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT = {'CDWZQ-2标-龙骨湾右线大桥-0-7号墩-平原': 'L156372', 'CDWZQ-2标-蓝家湾特大 桥-31-31-平原': 'L159206'}
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST = list(global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.keys())
# 点击测量导航栏按钮
measure_page_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_3_layout"))
)
measure_page_btn.click()
# 处理平差
self.screenshot_page.screenshot_page_manager(self.device_id)
# 检查是否有需要上传的断点
if not global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
logging.info(f"设备 {self.device_id} 断点列表为空,无需执行上传操作")
return False
# 点击上传导航栏按钮
upload_page_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout"))
)
upload_page_btn.click()
# 遍历断点列表,逐个执行上传操作
upload_success_count = 0
for breakpoint_name in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
try:
logging.info(f"设备 {self.device_id} 开始处理断点 '{breakpoint_name}' 的上传")
# 安全地获取断点信息
line_num = global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.get(breakpoint_name)
if line_num is None:
logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 在字典中未找到,跳过上传")
continue
if not line_num:
logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 未获取到line_num跳过上传")
continue
# 执行上传配置管理,传入当前断点名称
if self.upload_config_page.upload_config_page_manager(self.results_dir, breakpoint_name, line_num):
logging.info(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传成功")
upload_success_count += 1
else:
logging.error(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传失败")
except Exception as e:
logging.error(f"设备 {self.device_id} 处理断点 '{breakpoint_name}' 时发生异常: {str(e)}")
logging.info(f"设备 {self.device_id} 上传配置管理执行完成,成功上传 {upload_success_count}/{len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)} 个断点")
# 如果所有断点都上传成功返回True否则返回False
all_upload_success = upload_success_count == len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)
if all_upload_success:
logging.info(f"设备 {self.device_id} 所有断点上传成功")
# 把上传成功的断点写入日志文件"上传成功的断点.txt"
with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f:
for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST:
f.write(f"{bp}\n")
else:
logging.warning(f"设备 {self.device_id} 部分断点上传失败")
# 把上传成功的断点写入日志文件"上传成功的断点.txt"
with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f:
for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST:
f.write(f"{bp}\n")
# 把上传失败的断点写入日志文件"上传失败的断点.txt"
with open(os.path.join(self.results_dir, "上传失败的断点.txt"), "w", encoding='utf-8') as f:
for bp in set(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)-set(global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST):
f.write(f"{bp}\n")
return all_upload_success
except Exception as e:
logging.error(f"设备 {self.device_id} 处理应用状态时出错: {str(e)}")
return False
def check_and_click_confirm_popup_appium(self):
"""
适用于Appium的弹窗检测函数
Returns:
bool: 是否成功处理弹窗
"""
try:
from appium.webdriver.common.appiumby import AppiumBy
# 使用self.driver而不是参数
if not hasattr(self, 'driver') or self.driver is None:
logging.warning("driver未初始化无法检测弹窗")
return False
# 检查弹窗消息
message_elements = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.TextView[@text="是否退出测量界面?"]')
if message_elements:
logging.info("检测到退出测量界面弹窗")
# 点击"是"按钮
confirm_buttons = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.Button[@text="" and @resource-id="android:id/button1"]')
if confirm_buttons:
confirm_buttons[0].click()
logging.info("已点击''按钮")
time.sleep(1)
return True
else:
logging.warning("未找到''按钮")
return False
else:
return False
except Exception as e:
logging.error(f"Appium检测弹窗时发生错误: {str(e)}")
return False
def navigate_to_login_page(self, driver, device_id):
"""
补充的跳转页面函数:当设备处于未知状态时,尝试跳转到登录页面
参数:
driver: 已初始化的Appium WebDriver对象
device_id: 设备ID用于日志记录
"""
try:
target_package = 'com.bjjw.cjgc'
target_activity = '.activity.LoginActivity'
# 使用ADB命令启动Activity
try:
logging.info(f"尝试使用ADB命令启动LoginActivity: {target_package}/{target_activity}")
adb_command = f"adb -s {device_id} shell am start -n {target_package}/{target_activity}"
result = subprocess.run(adb_command, shell=True, capture_output=True, text=True)
if result.returncode == 0:
logging.info(f"使用ADB命令启动LoginActivity成功")
time.sleep(2) # 等待Activity启动
return True
else:
logging.warning(f"ADB命令执行失败: {result.stderr}")
except Exception as adb_error:
logging.warning(f"执行ADB命令时出错: {adb_error}")
except Exception as e:
logging.error(f"跳转到登录页面过程中发生未预期错误: {e}")
# 所有尝试都失败
return False
def run_automation(self):
"""运行自动化流程"""
try:
success = self.handle_app_state()
# success = self.test_handle_app_state()
if success:
logging.info(f"设备 {self.device_id} 自动化流程执行成功")
else:
logging.error(f"设备 {self.device_id} 自动化流程执行失败")
return success
except Exception as e:
logging.error(f"设备 {self.device_id} 自动化执行过程中发生错误: {str(e)}")
return False
finally:
self.quit()
def quit(self):
"""关闭驱动"""
safe_quit_driver(getattr(self, 'driver', None), self.device_id)
# 主执行逻辑
if __name__ == "__main__":
# 单个设备配置 - 现在DeviceAutomation会自动获取设备ID
try:
automation = DeviceAutomation()
success = automation.run_automation()
if success:
logging.info(f"设备自动化流程执行成功")
else:
logging.error(f"设备自动化流程执行失败")
except Exception as e:
logging.error(f"设备执行出错: {str(e)}")

BIN
music/901135.wav Normal file

Binary file not shown.

0
page_objects/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

46
page_objects/call_xie.py Normal file
View File

@@ -0,0 +1,46 @@
import socket
def send_tcp_command(command: str, host: str = '127.0.0.1', port: int = 8888, encoding: str = 'utf-8') -> bool:
"""
向指定TCP端口发送指令
参数:
command: 要发送的指令字符串
host: 目标主机地址默认127.0.0.1
port: 目标端口默认8888
encoding: 字符串编码格式默认utf-8
返回:
发送成功返回True失败返回False
"""
# 创建TCP socket并自动关闭with语句确保资源释放
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
# 连接服务器超时时间5秒避免无限阻塞
sock.settimeout(5.0)
sock.connect((host, port))
# 发送指令(转换为字节流)
sock.sendall(command.encode(encoding))
print(f"指令 '{command}' 发送成功")
return True
except ConnectionRefusedError:
print(f"连接失败:{host}:{port} 未监听或不可达")
except socket.timeout:
print(f"连接超时超过5秒未连接到 {host}:{port}")
except UnicodeEncodeError:
print(f"编码失败:指令包含{encoding}无法编码的字符")
except Exception as e:
print(f"发送失败:{str(e)}")
return False
# 使用示例
if __name__ == "__main__":
# 发送StartConnect指令
send_tcp_command("StartConnect")
# 也可以发送其他指令,例如:
# send_tcp_command("StopConnect")

View File

@@ -0,0 +1,319 @@
# 更新基站页面操作
# page_objects/download_tabbar_page.py
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException
import logging
import time
from datetime import datetime
import globals.ids as ids # 导入元素ID
import globals.global_variable as global_variable # 导入全局变量
from globals.driver_utils import check_session_valid, reconnect_driver
class DownloadTabbarPage:
def __init__(self, driver, wait, device_id):
self.driver = driver
self.wait = wait
self.device_id = device_id
self.logger = logging.getLogger(__name__)
# 添加默认的目标日期值
self.target_year = 2022
self.target_month = 9
self.target_day = 22
def is_download_tabbar_visible(self):
"""检查下载标签栏是否可见"""
try:
return self.driver.find_element(AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID).is_displayed()
except NoSuchElementException:
self.logger.warning("下载标签栏元素未找到")
return False
except Exception as e:
self.logger.error(f"检查下载标签栏可见性时发生意外错误: {str(e)}")
return False
def click_download_tabbar(self):
"""点击下载标签栏"""
try:
download_tab = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID))
)
download_tab.click()
self.logger.info("已点击下载标签栏")
# 使用显式等待替代固定等待
self.wait.until(
lambda driver: self.is_download_tabbar_visible()
)
return True
except TimeoutException:
self.logger.error("等待下载标签栏可点击超时")
return False
except Exception as e:
self.logger.error(f"点击下载标签栏时出错: {str(e)}")
return False
def update_work_base(self):
"""更新工作基点"""
try:
# 点击更新工作基点
update_work_base = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_WORK_BASE))
)
update_work_base.click()
self.logger.info("已点击更新工作基点")
# 等待更新完成 - 可以添加更具体的等待条件
# 例如等待某个进度条消失或成功提示出现
time.sleep(2) # 暂时保留,但建议替换为显式等待
return True
except TimeoutException:
self.logger.error("等待更新工作基点按钮可点击超时")
return False
except Exception as e:
self.logger.error(f"更新工作基点时出错: {str(e)}")
return False
def _get_current_date(self):
"""获取当前开始日期控件的日期值,支持多种格式解析"""
try:
date_element = self.wait.until(
EC.visibility_of_element_located((AppiumBy.ID, ids.DATE_START))
)
date_text = date_element.text.strip()
self.logger.info(f"获取到当前开始日期: {date_text}")
# 尝试多种日期格式解析
date_formats = [
"%Y-%m-%d", # 匹配 '2025-08-12' 格式
"%Y年%m月%d", # 匹配 '2025年08月12日' 格式
"%Y/%m/%d" # 可选:添加其他可能的格式
]
for fmt in date_formats:
try:
return datetime.strptime(date_text, fmt)
except ValueError:
continue # 尝试下一种格式
# 如果所有格式都匹配失败
self.logger.error(f"日期格式解析错误: 无法识别的格式,日期文本: {date_text}")
return None
except TimeoutException:
self.logger.error("获取当前日期超时")
return None
except Exception as e:
self.logger.error(f"获取当前日期失败: {str(e)}")
return None
def update_level_line(self):
"""更新水准线路修改为设置2022年9月22日"""
try:
# 点击更新水准线路
update_level_line = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE))
)
update_level_line.click()
self.logger.info("已点击更新水准线路")
# 获取原始开始日期
original_date = self._get_current_date()
if not original_date:
self.logger.error("无法获取原始开始日期,更新水准线路失败")
return False
# 点击开始日期
date_start = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.DATE_START))
)
date_start.click()
self.logger.info("已点击开始日期控件")
# 处理时间选择器设置为2022年9月22日
if not self.handle_time_selector(2022, 9, 22, original_date):
self.logger.error("处理时间选择失败")
return False
return True
except TimeoutException:
self.logger.error("等待更新水准线路按钮可点击超时")
return False
except Exception as e:
self.logger.error(f"更新水准线路时出错: {str(e)}")
return False
def _scroll_to_value(self, picker_id, target_value, original_value, max_attempts=20):
"""滚动选择器到目标值,基于原始值计算滚动次数"""
try:
# 计算需要滚动的次数(绝对值)
scroll_count = abs(int(target_value) - int(original_value))
self.logger.info(f"需要滚动{scroll_count}次将{picker_id}{original_value}调整到{target_value}")
# 确定滚动方向
direction = "down" if int(target_value) > int(original_value) else "up"
# 获取选择器元素
picker = self.wait.until(
EC.visibility_of_element_located((AppiumBy.ID, picker_id))
)
# 计算滚动坐标
x = picker.location['x'] + picker.size['width'] // 2
self.logger.info(f"水平位置x为{x}")
y_center = picker.location['y'] + picker.size['height'] // 2
self.logger.info(f"垂直位置中点y_center为{y_center}")
# start_y = y_center if direction == "down" else picker.location['y']
# end_y = picker.location['y'] if direction == "down" else y_center
# 关键修改:计算选择器高度的五分之一(滑动距离)
height_fifth = picker.size['height'] // 5 # 1/5高度
# 根据方向计算起点和终点,确保滑动距离为 height_fifth
if direction == "down":
# 向下滚动从中心点向上滑动1/5高度
start_y = y_center
end_y = y_center - height_fifth # 终点 = 中心点 - 1/5高度
self.logger.info(f"down垂直开始位置start_y为{y_center}垂直结束位置end_y为{end_y}")
else:
# 向上滚动从中心点向下滑动1/5高度
start_y = y_center
end_y = y_center + height_fifth # 终点 = 中心点 + 1/5高度
self.logger.info(f"up垂直开始位置start_y为{y_center}垂直结束位置end_y为{end_y}")
# 执行滚动操作
for _ in range(scroll_count):
self.driver.swipe(x, start_y, x, end_y, 500)
time.sleep(0.5) # 等待滚动稳定
return True # 循环scroll_count次后直接返回
# # 验证当前值
# current_value = picker.text
# if current_value == str(target_value):
# self.logger.info(f"{picker_id}已达到目标值: {target_value}")
# return True
# 最终验证
# final_value = picker.text
# if final_value == str(target_value):
# self.logger.info(f"{picker_id}已达到目标值: {target_value}")
# return True
# else:
# self.logger.error(f"{picker_id}滚动{scroll_count}次后未达到目标值,当前值: {final_value}")
# return False
except StaleElementReferenceException:
self.logger.warning("元素状态已过期,重新获取")
return False
except Exception as e:
self.logger.error(f"滚动选择器出错: {str(e)}")
return False
def handle_time_selector(self, target_year, target_month, target_day, original_date=None):
"""处理时间选择器,选择起始时间并确认"""
self.logger.info(f"传入handle_time_selector的初始日期: {original_date}")
try:
# 等待时间选择器出现
self.wait.until(
EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG))
)
self.logger.info("时间选择对话框已出现")
# 如果没有提供原始日期,使用当前日期控件的值
if not original_date:
original_date = self._get_current_date()
if not original_date:
self.logger.error("无法获取原始日期,处理时间选择失败")
return False
# 滚动选择年份
if not self._scroll_to_value(ids.SCRCOLL_YEAR, target_year, original_date.year):
self.logger.error("滚动选择年份失败")
return False
# 滚动选择月份
if not self._scroll_to_value(ids.SCRCOLL_MONTH, target_month, original_date.month):
self.logger.error("滚动选择月份失败")
return False
# 滚动选择日期
if not self._scroll_to_value(ids.SCRCOLL_DAY, target_day, original_date.day):
self.logger.error("滚动选择日期失败")
return False
# 点击确认按钮
confirm_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.SCRCOLL_CONFIRM)) # 日期选择器确认按钮
)
confirm_btn.click()
self.logger.info("已确认时间选择")
# 点击对话框确认按钮UPDATE_LEVEL_LINE_CONFIRM
confirm_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE_CONFIRM)) # 选择日期对话框确认按钮
)
confirm_btn.click()
self.logger.info("已点击对话框确认按钮commit")
# 新增:等待加载对话框出现
custom_dialog = self.wait.until(
EC.visibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG))
)
self.logger.info("检测到loading对话框出现")
if not check_session_valid(self.driver, self.device_id):
self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...")
if not reconnect_driver(self.device_id, self.driver):
self.logger.error(f"设备 {self.device_id} 驱动重连失败")
# 新增:等待加载对话框消失(表示更新完成)
WebDriverWait(self.driver, 300).until(
EC.invisibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG))
)
self.logger.info("loading对话框已消失更新完成")
'''点击commit确认按钮后loading弹窗会出现等待其加载完成后关闭
检测导航栏中的测量tabbar是否出现来确定是否返回True
'''
# measure_tabbar_btn = self.wait.until(
# EC.visibility_of_element_located((AppiumBy.ID, ids.MEASURE_TABBAR_ID))
# )
# self.logger.info("检测测量tabbar按钮出现")
return True
except TimeoutException as e:
# 明确超时发生在哪个环节
self.logger.error("处理时间选择时超时:可能是等待对话框出现、日期选择器元素或确认按钮超时", exc_info=True)
return False
except Exception as e:
# 细分不同环节的异常
self.logger.error(f"处理时间选择对话框时出错:可能是日期滚动、选择器确认或对话框确认环节失败 - {str(e)}", exc_info=True)
return False
def download_tabbar_page_manager(self):
"""执行基础更新操作"""
try:
# 执行基础更新流程
self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 开始执行更新流程")
# 点击下载标签栏
if not self.click_download_tabbar():
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 点击下载标签栏失败")
return False
# 更新工作基点
if not self.update_work_base():
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新工作基点失败")
return False
# 更新水准线路
if not self.update_level_line():
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新水准线路失败")
return False
self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新操作执行成功")
return True
except Exception as e:
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 执行更新操作时出错: {str(e)}")
return False

View File

@@ -0,0 +1,78 @@
# 登录页面操作
# page_objects/login_page.py
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import logging
import time
import globals.ids as ids
import globals.global_variable as global_variable # 导入全局变量模块
class LoginPage:
def __init__(self, driver, wait):
self.driver = driver
self.wait = wait
self.logger = logging.getLogger(__name__)
def is_login_page(self):
"""检查当前是否为登录页面"""
try:
return self.driver.find_element(AppiumBy.ID, ids.LOGIN_BTN).is_displayed()
except NoSuchElementException:
return False
def login(self):
"""执行登录操作"""
try:
self.logger.info("正在执行登录操作...")
# 获取文本框中已有的用户名
username_field = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME))
)
# 读取文本框内已有的用户名(.text属性获取元素显示的文本内容
existing_username = username_field.text
# 3. 将获取到的用户名写入全局变量中
global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值
# 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解)
if existing_username.strip(): # 去除空格后判断是否有有效内容
self.logger.info(f"已获取文本框中的已有用户名: {existing_username}")
else:
self.logger.info("文本框中未检测到已有用户名(内容为空)")
# 点击登录按钮
login_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_BTN))
)
login_btn.click()
self.logger.info("已点击登录按钮")
# 等待登录完成
time.sleep(3)
# 检查是否登录成功
if self.is_login_successful():
self.logger.info("登录成功")
return True
else:
self.logger.warning("登录后未检测到主页面元素")
return False
except Exception as e:
self.logger.error(f"登录过程中出错: {str(e)}")
return False
def is_login_successful(self):
"""检查登录是否成功"""
try:
# 等待主页面元素出现
self.wait.until(
EC.presence_of_element_located((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID))
)
return True
except TimeoutException:
return False

View File

@@ -0,0 +1,506 @@
# test_more_download_page.py
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import logging
import time
from globals.driver_utils import launch_app_manually
from page_objects.login_page import LoginPage
class MoreDownloadPage:
def __init__(self, driver, wait,device_id):
self.driver = driver
self.wait = wait
self.device_id = device_id
self.logger = logging.getLogger(__name__)
def is_on_more_download_page(self):
"""通过下载历史数据按钮来判断是否在更多下载页面"""
try:
# 使用下载历史数据按钮的resource-id来检查
download_history_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/download_history")
self.wait.until(EC.presence_of_element_located(download_history_locator))
self.logger.info("已确认在更多下载页面")
return True
except TimeoutException:
self.logger.warning("未找到下载历史数据按钮,不在更多下载页面")
return False
except Exception as e:
self.logger.error(f"检查更多下载页面时发生意外错误: {str(e)}")
return False
def click_download_button(self):
"""点击下载按钮"""
try:
# 点击下载历史数据按钮
download_button = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_history"))
)
download_button.click()
self.logger.info("已点击下载历史数据按钮")
# 等待下载操作开始
# time.sleep(3)
return True
except TimeoutException:
self.logger.error("等待下载按钮可点击超时")
return False
except Exception as e:
self.logger.error(f"点击下载按钮时出错: {str(e)}")
return False
def click_download_original_data(self):
"""点击下载原始数据按钮并处理日期选择"""
try:
# 点击下载原始数据按钮
download_original_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_org"))
)
download_original_btn.click()
self.logger.info("已点击下载原始数据按钮")
# 等待日期选择弹窗出现
# time.sleep(2)
# 点击选择开始日期
start_date_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date"))
)
start_date_btn.click()
self.logger.info("已点击选择开始日期")
# 等待日期选择器出现
# time.sleep(2)
# 滑动年份选择器 - 向上滑动1/5的距离
if not self._swipe_year_wheel():
self.logger.error("滑动年份选择器失败")
return False
# 点击日期选择器的确定按钮
confirm_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn"))
)
confirm_btn.click()
self.logger.info("已确认日期选择")
# 等待日期选择器关闭
# time.sleep(2)
# 假设弹窗有确定按钮,点击它开始下载
try:
# 尝试查找并点击下载弹窗的确定按钮
download_confirm_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]"))
)
download_confirm_btn.click()
self.logger.info("已点击下载确认按钮")
except TimeoutException:
self.logger.warning("未找到下载确认按钮,可能不需要确认")
# 等待下载开始
# time.sleep(3)
return True
except TimeoutException:
self.logger.error("等待下载原始数据按钮可点击超时")
return False
except Exception as e:
self.logger.error(f"点击下载原始数据时出错: {str(e)}")
return False
def click_download_result_data(self):
"""点击下载成果数据按钮并处理日期选择"""
try:
# 点击下载成果数据按钮
download_result_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_result"))
)
download_result_btn.click()
self.logger.info("已点击下载成果数据按钮")
# 等待日期选择弹窗出现
# time.sleep(2)
# 点击选择开始日期
start_date_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date"))
)
start_date_btn.click()
self.logger.info("已点击选择开始日期")
# 等待日期选择器出现
# time.sleep(2)
# 滑动年份选择器 - 向上滑动1/5的距离
if not self._swipe_year_wheel():
self.logger.error("滑动年份选择器失败")
return False
# 点击日期选择器的确定按钮
confirm_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn"))
)
confirm_btn.click()
self.logger.info("已确认日期选择")
# 等待日期选择器关闭
# time.sleep(2)
# 假设弹窗有确定按钮,点击它开始下载
try:
# 尝试查找并点击下载弹窗的确定按钮
download_confirm_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]"))
)
download_confirm_btn.click()
self.logger.info("已点击下载确认按钮")
except TimeoutException:
self.logger.warning("未找到下载确认按钮,可能不需要确认")
# 等待下载开始
# time.sleep(3)
return True
except TimeoutException:
self.logger.error("等待下载成果数据按钮可点击超时")
return False
except Exception as e:
self.logger.error(f"点击下载成果数据时出错: {str(e)}")
return False
def _swipe_year_wheel(self):
"""滑动年份选择器的滚轮"""
try:
# 获取年份选择器滚轮元素
year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView2")
# 获取滚轮的位置和尺寸
location = year_wheel.location
size = year_wheel.size
# 计算滚轮中心点坐标
center_x = location['x'] + size['width'] // 2
center_y = location['y'] + size['height'] // 2
# 计算滑动距离 - 滚轮高度的1/5
swipe_distance = size['height'] // 5
for i in range(3):
# 执行滑动操作 - 从中心向上滑动1/5高度
self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500)
self.logger.info(f"{i}次滑动月份选择器")
return True
except Exception as e:
self.logger.error(f"滑动年份选择器时出错: {str(e)}")
return False
# def wait_for_loading_dialog(self, timeout=900, download_type="unknown"):
# """
# 检查特定结构的加载弹窗的出现和消失
# 参数:
# timeout: 最大等待时间默认10分钟(600秒)
# 返回:
# bool: 如果加载弹窗出现并消失返回True否则返回False
# """
# try:
# self.logger.info(f"开始检查{download_type}加载弹窗...")
# # 首先检查加载弹窗是否出现
# start_time = time.time()
# loading_appeared = False
# # 等待加载弹窗出现最多等待30秒
# while time.time() - start_time < 30:
# try:
# # 根据提供的结构查找加载弹窗
# # 查找包含ProgressBar和"loading..."文本的弹窗
# loading_indicators = [
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"),
# (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
# (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
# ]
# for by, value in loading_indicators:
# try:
# element = self.driver.find_element(by, value)
# if element.is_displayed():
# loading_appeared = True
# self.logger.info("数据下载已开始")
# self.logger.info("检测到加载弹窗出现")
# break
# except:
# continue
# if loading_appeared:
# break
# except Exception as e:
# pass
# time.sleep(1)
# # 如果加载弹窗没有出现直接返回True
# if not loading_appeared:
# self.logger.info("未检测到加载弹窗,继续执行")
# return True
# # 等待加载弹窗消失
# self.logger.info("等待加载弹窗消失...")
# disappearance_start_time = time.time()
# while time.time() - disappearance_start_time < timeout:
# try:
# # 检查加载弹窗是否还存在
# loading_still_exists = False
# for by, value in loading_indicators:
# try:
# element = self.driver.find_element(by, value)
# if element.is_displayed():
# loading_still_exists = True
# break
# except:
# continue
# if not loading_still_exists:
# self.logger.info("加载弹窗已消失")
# return True
# # 每1分钟记录一次状态
# if int(time.time() - disappearance_start_time) % 60 == 0:
# elapsed_time = int(time.time() - disappearance_start_time)
# self.logger.info(f"加载弹窗仍在显示,已等待{elapsed_time//60}分钟")
# except Exception as e:
# # 如果出现异常,可能弹窗已经消失
# self.logger.info("加载弹窗可能已消失")
# return True
# time.sleep(1)
# # 如果超时记录错误并返回False
# self.logger.error(f"加载弹窗在{timeout}秒后仍未消失")
# # return False
# # 检查是否有loading加载窗口
# try:
# self.logger.info(f"检查{download_type}下载是否有loading加载窗口...")
# loading_indicators = [
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"),
# (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
# (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
# ]
# loading_exists = False
# for by, value in loading_indicators:
# try:
# element = self.driver.find_element(by, value)
# if element.is_displayed():
# loading_exists = True
# break
# except:
# continue
# if loading_exists:
# self.logger.info(f"检测到{download_type}下载的loading加载窗口执行重新打开应用操作")
# # 手动启动应用
# launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity")
# self.logger.info("已重新启动沉降观测应用")
# # 点击登录
# login_page = LoginPage(self.driver, self.wait)
# if login_page.is_login_page():
# if login_page.login():
# self.logger.info("登录成功")
# else:
# self.logger.error("登录失败")
# return False
# else:
# self.logger.info("应用已登录,无需重复登录")
# self.logger.info(f"{download_type}下载超时处理完成,应用已重启")
# # 点击img_5_layout更多下载按钮
# more_download_btn = self.wait.until(
# EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout"))
# )
# more_download_btn.click()
# self.logger.info("已点击更多下载按钮")
# self.more_download_page_manager_2(download_type)
# # 等待页面加载
# time.sleep(1)
# else:
# self.logger.info(f"未检测到{download_type}下载的loading加载窗口")
# except Exception as e:
# self.logger.error(f"检查{download_type}下载的loading加载窗口时出错: {str(e)}")
# # 出错时继续执行,不影响主流程
# return False
# except Exception as e:
# self.logger.error(f"检查加载弹窗时出错: {str(e)}")
# return False
def _is_loading_present(self):
"""私有辅助方法:检测当前页面是否存在加载弹窗"""
loading_indicators = [
(AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
(AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
(AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
]
for by, value in loading_indicators:
try:
element = self.driver.find_element(by, value)
if element.is_displayed():
return True
except:
continue
return False
def wait_for_loading_dialog(self, timeout=900, download_type="unknown", retry_count=0):
"""
检查加载弹窗的出现和消失支持最多1次重试总共执行2次
"""
try:
self.logger.info(f"开始检查 {download_type} 加载弹窗 (尝试次数: {retry_count + 1})...")
# 1. 等待加载弹窗出现最多30秒
start_time = time.time()
loading_appeared = False
while time.time() - start_time < 30:
if self._is_loading_present():
loading_appeared = True
self.logger.info(f"检测到 {download_type} 加载弹窗出现")
break
time.sleep(1)
if not loading_appeared:
self.logger.info(f"未检测到 {download_type} 加载弹窗,视为直接通过")
return True
# 2. 等待加载弹窗消失
disappearance_start_time = time.time()
while time.time() - disappearance_start_time < timeout:
if not self._is_loading_present():
self.logger.info(f"{download_type} 加载弹窗已消失")
return True
# 每60秒打印一次日志
if int(time.time() - disappearance_start_time) % 60 == 0:
self.logger.info(f"等待中...已耗时 {int(time.time() - disappearance_start_time)//60} 分钟")
time.sleep(2)
# 3. 超时处理逻辑
self.logger.error(f"{download_type} 加载弹窗在 {timeout} 秒后仍未消失")
# 检查是否还可以重试retry_count=0 时执行重试,即第二次执行)
if retry_count < 1:
self.logger.warning(f"检测到超时,准备进行第 {retry_count + 2} 次尝试(重启应用)...")
# 执行重启逻辑
from globals.driver_utils import launch_app_manually
from page_objects.login_page import LoginPage
launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity")
login_page = LoginPage(self.driver, self.wait)
if login_page.is_login_page():
login_page.login()
# 重新导航到下载页面
more_download_btn = self.wait.until(
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout"))
)
more_download_btn.click()
# 递归调用:增加 retry_count
return self.more_download_page_manager_2(download_type, retry_count = retry_count + 1)
else:
self.logger.error(f"{download_type} 已达到最大重试次数,操作失败")
return False
except Exception as e:
self.logger.error(f"检查加载弹窗时发生异常: {str(e)}")
return False
def more_download_page_manager_2(self, download_type, retry_count=0):
"""
修改后的 manager_2透传 retry_count 参数
"""
try:
if not self.is_on_more_download_page():
return False
self.click_download_button()
if download_type in ["原始数据", "历史数据"]:
self.click_download_original_data()
return self.wait_for_loading_dialog(download_type="原始数据", retry_count=retry_count)
elif download_type == "成果数据":
self.click_download_result_data()
return self.wait_for_loading_dialog(download_type="成果数据", retry_count=retry_count)
return False
except Exception as e:
self.logger.error(f"manager_2 执行出错: {str(e)}")
return False
def more_download_page_manager(self):
"""执行更多下载页面管理操作"""
try:
self.logger.info("开始执行更多下载页面操作")
# 检查是否在更多下载页面
if not self.is_on_more_download_page():
self.logger.error("不在更多下载页面")
return False
# 点击下载历史数据按钮
if not self.click_download_button():
self.logger.error("点击下载历史数据按钮失败")
return False
# 等待下载历史数据页面加载完成
# time.sleep(3)
# 点击下载原始数据按钮
if not self.click_download_original_data():
self.logger.error("点击下载原始数据按钮失败")
return False
# 等待下载操作完成
time.sleep(1)
# 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失
if not self.wait_for_loading_dialog(download_type="原始数据"):
self.logger.warning("下载过程中的加载弹窗未在预期时间内消失,但操作已完成")
# 等待一段时间,确保原始数据下载完成
time.sleep(1)
# 点击下载成果数据按钮
if not self.click_download_result_data():
self.logger.error("点击下载成果数据按钮失败")
return False
# 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失
if not self.wait_for_loading_dialog(download_type="成果数据"):
self.logger.warning("成果数据下载过程中的加载弹窗未在预期时间内消失,但操作已完成")
self.logger.info("更多下载页面操作执行完成")
return True
except Exception as e:
self.logger.error(f"执行更多下载页面操作时出错: {str(e)}")
return False

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

251
permissions.py Normal file
View File

@@ -0,0 +1,251 @@
# 权限处理
import subprocess
import logging
import time
# 配置日志
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s")
def check_device_connection(device_id: str) -> bool:
"""检查设备连接状态"""
try:
check_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.product.model"]
result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
if result.returncode == 0:
logging.info(f"设备 {device_id} 连接正常,型号: {result.stdout.strip()}")
return True
else:
logging.error(f"设备 {device_id} 连接失败: {result.stderr.strip()}")
return False
except subprocess.TimeoutExpired:
logging.error(f"设备 {device_id} 连接超时")
return False
except Exception as e:
logging.error(f"检查设备 {device_id} 时发生错误: {str(e)}")
return False
def is_package_installed(device_id: str, package_name: str) -> bool:
"""检查包是否已安装"""
try:
check_cmd = ["adb", "-s", device_id, "shell", "pm", "list", "packages", package_name]
result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
return result.returncode == 0 and package_name in result.stdout
except Exception as e:
logging.error(f"检查包 {package_name} 时发生错误: {str(e)}")
return False
def grant_single_permission(device_id: str, package: str, permission: str) -> bool:
"""
为单个包授予单个权限
:return: 是否成功授予
"""
try:
grant_cmd = [
"adb", "-s", device_id,
"shell", "pm", "grant", package,
permission
]
result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15)
if result.returncode == 0:
logging.info(f"设备 {device_id}:已成功授予 {package} 权限: {permission}")
return True
else:
error_msg = result.stderr.strip()
logging.warning(f"设备 {device_id}:授予 {package} 权限 {permission} 失败: {error_msg}")
# 尝试使用root权限
if "security" in error_msg.lower() or "permission" in error_msg.lower():
logging.info(f"设备 {device_id}尝试使用root权限授予 {package} 权限")
# 重启adb为root模式
root_cmd = ["adb", "-s", device_id, "root"]
subprocess.run(root_cmd, capture_output=True, text=True, timeout=10)
time.sleep(2) # 等待root权限生效
# 再次尝试授予权限
result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15)
if result.returncode == 0:
logging.info(f"设备 {device_id}使用root权限成功授予 {package} 权限: {permission}")
return True
else:
logging.error(f"设备 {device_id}即使使用root权限也无法授予 {package} 权限 {permission}: {result.stderr.strip()}")
return False
else:
return False
except subprocess.CalledProcessError as e:
logging.error(f"设备 {device_id}ADB 命令执行失败,返回码 {e.returncode}")
logging.error(f"标准输出:{e.stdout.strip()}")
logging.error(f"错误输出:{e.stderr.strip()}")
return False
except Exception as e:
logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}")
return False
def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool:
"""
为 Appium UiAutomator2 服务授予权限
:param device_id: 设备 ID
:param require_all: 是否要求所有权限都成功授予
:return: 权限授予是否成功根据require_all参数判断
"""
# 首先检查设备连接
if not check_device_connection(device_id):
return False
packages_to_grant = [
"io.appium.settings",
"io.appium.uiautomator2.server",
"io.appium.uiautomator2.server.test"
]
# 权限列表(按优先级排序)
permissions_to_grant = [
"android.permission.WRITE_SECURE_SETTINGS",
"android.permission.CHANGE_CONFIGURATION",
"android.permission.DUMP",
]
success_count = 0
total_attempted = 0
package_results = {}
# 检查并授予权限
for package in packages_to_grant:
package_results[package] = {"installed": False, "permissions": {}}
if not is_package_installed(device_id, package):
logging.warning(f"设备 {device_id}:包 {package} 未安装,跳过权限授予")
package_results[package]["installed"] = False
continue
package_results[package]["installed"] = True
package_success = 0
package_attempted = 0
for permission in permissions_to_grant:
total_attempted += 1
package_attempted += 1
result = grant_single_permission(device_id, package, permission)
package_results[package]["permissions"][permission] = result
if result:
success_count += 1
package_success += 1
# 记录每个包的授权结果
logging.info(f"设备 {device_id}:包 {package} 权限授予结果: {package_success}/{package_attempted}")
# 统计和报告
logging.info(f"设备 {device_id}:权限授予完成")
logging.info(f"总计: 尝试 {total_attempted} 次,成功 {success_count}")
# 检查每个包的关键权限
critical_permission = "android.permission.WRITE_SECURE_SETTINGS"
critical_failures = []
for package, info in package_results.items():
if info["installed"] and critical_permission in info["permissions"]:
if not info["permissions"][critical_permission]:
critical_failures.append(package)
if critical_failures:
logging.warning(f"设备 {device_id}:以下包的关键权限 {critical_permission} 授予失败: {', '.join(critical_failures)}")
logging.warning("这可能会影响某些自动化功能,但基础测试通常不受影响")
# 根据require_all参数返回结果
if require_all:
# 要求所有权限都成功
if critical_failures:
logging.error("关键权限授予失败无法继续require_all=True")
return False
return success_count == total_attempted
else:
# 不要求所有权限只要设备连接正常就返回True
logging.info(f"设备 {device_id}权限授予过程完成建议重启设备或Appium服务使更改生效")
return True
def check_appium_compatibility(device_id: str) -> dict:
"""
检查Appium兼容性
:return: 兼容性报告字典
"""
try:
# 获取Android版本
version_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.build.version.release"]
result = subprocess.run(version_cmd, capture_output=True, text=True, timeout=10)
android_version = result.stdout.strip() if result.returncode == 0 else "未知"
report = {
"device_id": device_id,
"android_version": android_version,
"compatibility": "unknown",
"notes": [],
"suggestions": []
}
try:
version_num = float(android_version.split('.')[0])
if version_num >= 11:
report["compatibility"] = "limited"
report["notes"].append("Android 11+ 对WRITE_SECURE_SETTINGS权限限制非常严格")
report["suggestions"].append("使用--no-reset参数启动Appium")
report["suggestions"].append("设置autoGrantPermissions=false")
elif version_num >= 10:
report["compatibility"] = "moderate"
report["notes"].append("Android 10 限制了WRITE_SECURE_SETTINGS权限")
report["suggestions"].append("可尝试使用root权限的设备")
elif version_num >= 9:
report["compatibility"] = "good"
report["notes"].append("Android 9 兼容性较好")
else:
report["compatibility"] = "excellent"
report["notes"].append("Android 8或以下版本完全兼容")
except (ValueError, IndexError):
report["notes"].append("无法解析Android版本")
return report
except Exception as e:
logging.error(f"检查兼容性时出错: {str(e)}")
return {"device_id": device_id, "error": str(e)}
# 使用示例
if __name__ == "__main__":
# 获取设备ID示例
devices_cmd = ["adb", "devices"]
result = subprocess.run(devices_cmd, capture_output=True, text=True)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')[1:] # 跳过第一行标题
for line in lines:
if line.strip() and "device" in line:
device_id = line.split('\t')[0]
logging.info(f"找到设备: {device_id}")
# 1. 检查兼容性
report = check_appium_compatibility(device_id)
logging.info(f"兼容性报告: Android {report.get('android_version', '未知')} - {report.get('compatibility', '未知')}")
# 2. 授予权限(不要求全部成功)
success = grant_appium_permissions(device_id, require_all=False)
if success:
logging.info(f"设备 {device_id} 设置完成,可以开始测试")
else:
logging.warning(f"设备 {device_id} 权限授予有失败,但可能仍可进行基础测试")
# 3. 提供建议
if "suggestions" in report:
logging.info("建议:")
for suggestion in report["suggestions"]:
logging.info(f" - {suggestion}")
else:
logging.error("无法获取设备列表请确保ADB已正确安装且设备已连接")

View File

@@ -0,0 +1,5 @@
CDWZQ-3标-雷庙村大桥-8-号桥台-平原
CDWZQ-3标-雷庙村特大桥-5-6-号墩-平原
CDWZQ-3标-金马村特大桥-14-号墩-平原
CDWZQ-3标-雷庙村特大桥-4#-7-14号墩-平原
CDWZQ-3标-老屋坡特大桥-14-21-平原

View File

BIN
上传人员信息.xlsx Normal file

Binary file not shown.