我有一台三星的Galaxy Tab A 8.0 2019,它在我今年買入新的Galaxy Tab A9+平板以前,是我的主力平板。但是它的規格實在是不敷使用(2G RAM/32G ROM),這也是我之所以會換掉它的原因。
這台平板退役後,在經過整理之後,我決定在上面安裝Fotoo,改造成數位相框。但是後續就有這些問題了:
- 要如何知道它的電量多少?怎麼實現自動充電以及避免過充?
- 如何在平板開機時自動開啟Fotoo?
先從平板開始說起。我是在平板上安裝MacroDroid(我有買Pro,可以新建多個巨集;免費版能新建多少巨集我不知道),在上面建立若干巨集,用於觸發充電、開機啟動Fotoo,以及實現查詢電量功能。
然後是伺服器部分,這是用於觸發連接充電頭的小米智慧插座開啟之用,以及提供Web介面顯示平板電量。整體程式是使用Python寫成,而這也是我生平第一支Python程式。
from flask import Flask, request
import os, signal, sys, os.path
import threading, subprocess, schedule, time
import json, configparser
from miio import MiotDevice
from update_ip import update_ip_fields
import requests
base_dir = os.path.dirname(os.path.abspath(__file__))
my_name = os.path.basename(__file__)
config_file = os.path.join(base_dir, "config.ini")
if not os.path.isfile(config_file):
print(f"File 'config.ini' not found!\nMake sure that config.ini is in the same place as {my_name}!")
sys.exit(1)
else:
config = configparser.ConfigParser()
config.read(config_file)
tablet_ip = config['hwinfo']['tablet_ip']
def set_plug():
config = configparser.ConfigParser()
config.read(config_file)
plug_ip = config['hwinfo']['plug_ip']
plug_token = config['auth']['mi_token']
return MiotDevice(ip=plug_ip, token=plug_token)
def get_status():
status_url=f"http://{tablet_ip}:8080/battery"
try:
response = requests.get(status_url, timeout=3)
data = response.json()
bat_level = int(data.get("battery", -1))
charging = str(data.get('charging', "unknown"))
return bat_level, charging
print(f"{bat_level},{charging}")
except Exception as e:
return None, None
app = Flask(__name__)
@app.route('/trigger', methods=['POST'])
def trigger_charging():
remote_ip = request.remote_addr
if not remote_ip:
print("Cannot read source IP!")
return {"error": "unknown IP"}, 403
elif remote_ip != tablet_ip:
print("Source IP does not match!")
return {"error": "mismatched IP"}, 403
elif remote_ip == tablet_ip:
print("Correct source IP, continue...")
plug_charger = set_plug()
data = request.get_json()
if data and data.get("device") == "tablet" and data.get("action") == "charge":
print("Charge request received, open charger...")
try:
status_query = plug_charger.raw_command("get_properties", [{'siid':2,'piid':1}])
status = status_query[0]["value"]
if not status:
try:
plug_charger.raw_command("set_properties", [{'siid':2,'piid':1,'value':True}])
print("Started charging.")
return {"status": "charging started"}, 200
except Exception as error:
print("Failed starting charge, {error}")
return {"status": "error starting charge"}, 500
elif status:
print("Already charging, skip request.")
return {"status": "Already started charging"}, 200
except Exception as e:
print(f"Error occurred: {e}")
elif data and data.get("device") == "tablet" and data.get("action") == "discharge":
print("Charge completed, close charger...")
try:
status_query = plug_charger.raw_command("get_properties", [{'siid':2,'piid':1}])
status = status_query[0]["value"]
if status:
try:
plug_charger.raw_command("set_properties", [{'siid':2,'piid':1,'value':False}])
print("Stopped charging.")
return {"status": "charging stopped"}, 200
except:
print("Failed stopping charge, wait for smartplug to close automatically.")
return {"status": "error stopping charge"}, 500
elif not status:
print("Charger is already turned off, skip request.")
return {"status": "Already started charging"}, 200
except Exception as e:
print(f"Error occurred: {e}")
return {"error": "Invalid request"}, 400
@app.route('/status')
def show_status():
bat_level, charging = get_status()
if charging == "On" or charging == "開":
if bat_level == 100:
charging_text = "已充飽"
else:
charging_text = "充電中"
elif charging == "Off" or charging == "關":
charging_text = "非充電中"
elif charging == "unknown":
charging_text = "未知"
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>平板數位相框電力狀態</title>
<meta http-equiv="refresh" content="30">
</head>
<body>
<h2>平板電力狀態</h2>
<p>目前電量:{bat_level}%</p>
<p>充電狀態:{charging_text}</p>
</body></html>
"""
return html
def run_server():
#sys.stderr = open(os.devnull, 'w')
print("Started listening...")
app.run(host='0.0.0.0', port=5000, debug=False)
def cleanup():
print("Exiting...")
os._exit(0)
signal.signal(signal.SIGINT, lambda s, f: cleanup())
signal.signal(signal.SIGTERM, lambda s, f: cleanup())
set_plug()
update_ip_fields()
server_thread = threading.Thread(target=run_server)
server_thread.start()
別看我上面程式碼洋洋灑灑寫了落落長,這可是我邊寫邊請教Copilot,還有自己的數次嘗試後,才得出的結果。它的執行流程是這樣:
- 起始:執行update_ip.py的update_ip_fields子程式(也是我自己開發的),檢查並更新設定檔config.ini中的平板與智慧插座IP欄位是否有變動,若有則一併修改。
完成後,開始執行Flask Web伺服器,監聽5000連接埠,提供/trigger與/status兩個路由。 - 觸發充電:平板MacroDroid在達到設定電量後,自動對/trigger路由發送json。程式收到後做以下處理:
- 檢查來源IP:確認請求確實是從平板發出的,這是為了避免意外觸發。
- 檢查插座狀態:檢查插座是否開啟,若未開啟則執行開啟指令。
- 觸發斷電:充電完成後,一樣由MacroDroid自動對/trigger路由發送json。程式收到後即對插座發出斷電指令,過程同觸發充電。另外插座端設有充電保護機制,作為備用。
- 檢查狀態:由任何可使用瀏覽器的裝置開啟/status頁面,顯示目前平板的電池電量,以及充電狀態(每半分鐘自動重新整理一次)。
如此理論上便可以讓平板始終播放相片幻燈片,而不會因為電池沒電而中斷。同樣的程式也可以用於用作監視器的舊手機,只要上面有安裝MacroDroid並建立觸發開始/停止充電的巨集,就OK了。
那這個程式有什麼限制嗎?有,那就是智慧插座部分目前只支援小米的,其它廠家的暫不支援。因為我目前用的智慧插座就是小米的,我查到的CLI控制方法也是針對小米的。所以若換成別家的智慧插座,這個程式要嘛得修改成與該廠牌智慧插座匹配的控制邏輯,要嘛就是無用武之地。