情景概述
因本人家中工作电脑日常需要24小时开机,并且会跑一些奇奇怪怪的程序,不在家中时,关机或重启电脑的需求很常见,现在问题来了:
1、电脑正常运行未死机的情况下我们可以通过ssh远程连入,通过reboot/shutdown命令来重启电脑,当然了,通过微软系统自带远程桌面RDP(mstsc)来连接操作也可以,不过对于无需交互式反馈的场景而言,效率太低,一般不用;
2、电脑死机的情况下,ssh或其他远程软件均不工作,无法远程连接并控制,只能强制重启电脑解决,如果人不在电脑旁边,物理强制重启则做不到,所幸现在智能开机插座很普遍了,价格也偏移,用智能开机插座连接主机电源,再通过远程控制插座电源的开关状态,即可对电脑实施强制重启(注意:做到这一步需要电脑主板支持来电自启设置,并把此功能设为启用状态!)
解决了电脑死机通过开关插座电源来重启电脑的问题,那现在新问题又来了:能不能自动监测主力电脑的运行状态,如果发生死机情况则自动进行断电重启,而不是每次手动远程连接电脑时,才发现电脑死机了这一状况,答案当然是可以,因本人家中局域网有EXSI虚拟机控制设备,运行了NAS和软路由等多台Linux设备,其所运行的系统异常稳定。只需要在这些Linux设备上编写程序定时周期性监控主力电脑的状态和插座耗电功率,即可判断电脑是否死机,检测到死机,则发送重启插座电源的指令即可;
综上情况:本文的程序脚本就诞生了,帮助我一键监控主力电脑的运行状态,死机则执行插座断电重启。间接提高生产工作的效率!
Tips:一些需要无人值守不间断运行的程序可以设置开机自启,相关技术细节此处不再赘述!
注:本脚本程序2023年1月经测试适配“向日葵开机插座C1-2、C1-Pro”,其他未测试!
对应向日葵官网产品介绍页面网址:https://sunlogin.oray.com/hardware/socket-c2
控制向日葵插座主程序: sunlogin_plug 文件代码:
#!/bin/sh
#控制向日葵开机智能插座
SCRIPTPATH="$(dirname $0)"
requestTimeOut=3 #请求超时时间(单位:秒)
#controlHostURL="http://192.168.1.105:6767" #发送的主机地址,可抓包获取,也可以指定插座联网的局域网IP地址,端口通常为6767
controlTime="01270152" #接收端需要的参数time,可抓包获取
controlKey="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" #接收端需要的参数key,可抓包获取
statusUrl="${controlHostURL}/plug?_api=get_plug_status&index=0&time=${controlTime}&key=${controlKey}"
cntdownUrl="${controlHostURL}/plug?_api=plug_cntdown_get&time=${controlTime}&key=${controlKey}&index=0"
checkVersionUrl="${controlHostURL}/plug?key=${controlKey}&_api=get_plug_version&time=${controlTime}"
electricUrl="${controlHostURL}/plug?key=${controlKey}&_api=get_plug_electric&time=${controlTime}"
powerOnUrl="${controlHostURL}/plug?_api=set_plug_status&index=0&time=${controlTime}&key=${controlKey}&status=1"
powerOffUrl="${controlHostURL}/plug?_api=set_plug_status&index=0&time=${controlTime}&key=${controlKey}&status=0"
_sendRequest() {
local url="$1"
local responseData=$(curl -sSf --connect-timeout $requestTimeOut "$url" 2>/dev/null)
[ ! -z "$responseData" ] && echo "$responseData" || echo "{}"
}
#远端返回错误代码时调用(即result!=0),非请求过程本身的错误
responseInvolid() {
local msg="【失败】:请求失败,请检查key是否已过期!"
echo "$msg"
cat >>$SCRIPTPATH/sunlogin-error.log<<<"[$(date +'%Y%m%d %H:%M:%S')] ${msg}"
#exit 255
return 255
}
appendErrorLog() {
local logFile="$SCRIPTPATH/sunlogin-error.log"
echo "$*"
cat >>$logFile<<<"[$(date +'%Y-%m-%d %H:%M:%S')] appendErrorLog:$*"
}
getPlugStatus() {
local json=$(_sendRequest "$statusUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
local plugStatus=$(echo "$json"|jq '.response[]|.status')
if [[ "${1,,}" == "--flag" ]];then #仅返回状态标志:1->开关开启; 2->开关关闭
echo "$plugStatus"
else #返回可读的信息提示文本
if [ $plugStatus -eq 1 ];then
echo "【开启】电源开关处在开启状态!"
elif [];then
echo "【关闭】电源开关处在关闭状态!"
else
echo "【错误】开关状态未知!"
fi
fi
}
getPlugVersion() {
#获取插座固件版本信息
local json=$(_sendRequest "$checkVersionUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
echo "$json"
}
getPlugPower() {
#获取插座当前的耗电功率!
local json=$(_sendRequest "$electricUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
if [[ "${1,,}" == "--raw" ]];then #返回原始JSON数据
echo "$json"
else #返回可读的信息提示文本
local power=$(echo "$json"|jq -r '.power') #当前电功率
local current=$(echo "$json"|jq -r '.curr') #当前电流
local volume=$(echo "$json"|jq -r '.vol') #当前电量
printf "【耗电】当前电功率:%s W/h,电流:%s mA,电量:%s \n" $(echo "scale=2;$power/1000"|bc) $(echo "scale=2;$current/1000"|bc) $volume
fi
}
getPlugOpenCount() {
#获取插座今日开启次数
#注:此接口功能待定(暂时不知道是干嘛用的...)
local json=$(_sendRequest "$cntdownUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
if [[ "${1,,}" == "--raw" ]];then #返回原始JSON数据
echo "$json"
else #返回可读的信息提示文本
#echo "TODO..."
echo "$json"
fi
}
getPlugPowerTime() {
#获取插座今日通电时长!
local url="https://sl-api.oray.com/smartplugs/xxxxxxxxxxxx/electric?date=2023-1-27&r=0.39320349774540664" #注意此URL可通过抓包网页控制面板获取
local json=$(_sendRequest "$url")
echo "$json"
}
powerOnPlug() {
#开启插座
local json=$(_sendRequest "$powerOnUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
if [[ "${1,,}" == "--raw" ]];then #返回原始JSON数据
echo "$json"
else #返回可读的信息提示文本
local setStatus=$(echo "$json"|jq -r '.result' 2>/dev/null||echo "1")
[ "$setStatus" = "0" ] && echo "【成功】开启插座成功!" || echo "【失败】开启插座失败!"
fi
}
powerOffPlug() {
#关闭插座
local json=$(_sendRequest "$powerOffUrl")
local status=$(echo "$json"|jq -r '.result' 2>/dev/null)
if [ -z "$status" -o "$status" = "null" ];then
appendErrorLog "【请求错误】:请求的地址无效,请检查被控主机配置是否正确,并确保被控设备已接入网络!"
return 99
elif [ $status -ne 0 ];then
responseInvolid && return
fi
if [[ "${1,,}" == "--raw" ]];then #返回原始JSON数据
echo "$json"
else #返回可读的信息提示文本
local setStatus=$(echo "$json"|jq -r '.result' 2>/dev/null||echo "1")
[ "$setStatus" = "0" ] && echo "【成功】关闭插座成功!" || echo "【失败】关闭插座失败!"
fi
}
powerRebootPlug() {
#重开插座(关闭插座后再次开启,中间间隔30秒)
powerOffPlug
echo "延时等待中..."
sleep 30
powerOnPlug
}
#——————————————————————————————————————————————————————————————
#主逻辑开始:功能函数调用:
#getPlugStatus #获取插座开关所处状态
#getPlugOpenCount
#getPlugPower #获取插座当前功率
#powerOnPlug #开启插座
#powerOffPlug #关闭插座
#powerRebootPlug #重启插座
case "${1,,}" in
"on")
echo "开启插座..."
powerOnPlug
;;
"off")
echo "关闭插座..."
powerOffPlug
;;
"reboot")
echo "重启插座电源..."
powerRebootPlug
;;
"status")
echo "获取插座开关状态..."
getPlugStatus
;;
"version")
echo "获取插座固件版本..."
getPlugVersion
;;
"power")
echo "获取插座耗电功耗..."
getPlugPower
;;
"count")
echo "获取插座开关机次数..."
getPlugOpenCount
;;
"time")
echo "获取插座今日使用时长..."
getPlugPowerTime
;;
*)
:
#echo "Do Nothing"
;;
esac
sunlogin-plug运行截图:
检测电脑端口和插座耗电功耗定时任务任务脚本代码 check-homepc-crash :
#!/bin/sh
#检查家中电脑是否死机奔溃
#如果崩溃,借助向日葵开机插座重新开启电脑电源
#-----------------------------------------------
#注:本脚本两次检查之间不做任何延时操作,多次检查之间的时间间隔请通过计划任务(crontab)指定
# eg:
# */10 * * * * /path/to/check-homepc-crash #每隔10分钟检查电脑是否死机
#另:任务时间间隔也不宜设置得过短,间隔的时间必须足够让电脑从开机到启动ssh服务有足够的时间,否则会出问题,比如设置为1分钟检查一次(很显然电脑无法保证在1分钟内把所有服务启动完成!)
pushd "$(dirname $0)" &>/dev/null
SCRIPTPATH="$(dirname $0)"
source $SCRIPTPATH/sunlogin_plug
recordFile="./homepc-status-$(date +'%Y%m%d').txt" #失败记录标记文件,记录电脑检查失败状态,多次检查失败则考虑通过重开电源开关来重启电脑
minCheckCount=3 #检查到多少次失败时,重启电源(重开插座开关),建议值不小于2,否则会出问题,起反作用!
#(比如:电脑首次手动开机时,SSH服务还没来得及启动,就被本程序判定为电脑死机,从而导致电脑重启,电脑表示很无辜)
initRecordFile() {
#初始化记录日志文件(文件不存在则自动创建)
touch $recordFile
}
emptyRecordFile() {
#清空日志文件,目标主机检查成功或无需后续检测步骤时,需要清空记录文件
cat>$recordFile</dev/null
}
endCheckProcess() {
#中断检查进程,退出后续操作!
emptyRecordFile
exit 0
}
#——————————————————————————————主逻辑开始(检查电脑是否开机,并且是否死机)——————————————————————————————————————
initRecordFile
nc -w 3 -v 192.168.1.99 22 &>/dev/null #因本人在电脑上配置了Cygwin ssh端口开机自启动,如果端口能顺利连接则认定电脑状态OK(未死机)
homePCSSH=$?
[ $homePCSSH -eq 0 ] && echo "当前电脑Cygwin SSH在线,退出后续检查!" && endCheckProcess
echo "Cygwin SSH端口检测失败,即将通过电源插座获取电脑开机状态..."
plugStatus=$(getPlugStatus --flag) #获取插座是否开机
[ "$plugStatus" = "0" ] && echo "插座开关未开启,退出后续检查!" && endCheckProcess
echo "当前插座为开启状态,继续获取插座设备耗电状态,以识别电脑是否处在开机状态..."
powerData=$(getPlugPower --raw) #获取插座耗电状态
powerNumber=$(echo "$powerData"|jq -r '.power')
if [ $powerNumber -lt 70000 ];then #插座有耗电,但功率小于70wh,则认为是其他东西在耗电,不认为是电脑开机状态,退出检查
echo "插座耗电功率小于 70 w/h,可能电脑并未开机,退出后续检查!"
endCheckProcess
fi
echo "电脑SSH端口检测失败,且插座耗电功率在 70wh 以上,电脑可能已经死机,将记录本次检查状态!"
cat>>$recordFile<<<"【$(date +'%Y-%m-%d %H:%M:%S')】 HOMEPC端口检查失败,且插座耗电量超过阈值!"
#查看日志记录次数,达到最小检查次数则重启插座电源
recordFileLines=$(cat $recordFile|wc -l)
if [ $recordFileLines -ge $minCheckCount ];then
echo "失败记录次数达标,即将重启插座电源以使电脑重新启动!"
emptyRecordFile #清空日志文件
#powerRebootPlug #重启插座电源
cat >>./homepc-reboot-log.log<<<"【$(date +'%Y-%m-%d %H:%M:%S')】执行了重启插座电源操作!"
else
echo "当前失败记录第 $recordFileLines 次,等待下次检查!"
fi
使用说明:
将 sunlogin_plug
和check-homepc-crash
这两文件放置于同一目录下,并赋予可执行权限(chmod a+x *),并将 check-homepc-crash
加入crontab
定时任务计划即可(注意任务的间隔时间不宜设置得过短)。