舊平板改造數位相框&Python程式設計新人作

我有一台三星的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控制方法也是針對小米的。所以若換成別家的智慧插座,這個程式要嘛得修改成與該廠牌智慧插座匹配的控制邏輯,要嘛就是無用武之地。

發佈留言