Coverage for src/webui.py: 70%
63 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-23 14:35 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-07-23 14:35 +0000
1#!/usr/bin/env python3
2"""
3エアコン室外機冷却システムの Web UI です。
5Usage:
6 webapp.py [-c CONFIG] [-s CONTROL_HOST] [-p PUB_PORT] [-a ACTUATOR_HOST] [-l LOG_PORT] [-n COUNT] [-D] [-d]
8Options:
9 -c CONFIG : CONFIG を設定ファイルとして読み込んで実行します。 [default: config.yaml]
10 -s CONTROL_HOST : コントローラのホスト名を指定します。 [default: localhost]
11 -p PUB_PORT : ZeroMQ の Pub サーバーを動作させるポートを指定します。 [default: 2222]
12 -a ACTUATOR_HOST : アクチュエータのホスト名を指定します。 [default: localhost]
13 -l LOG_PORT : 動作ログを提供するアクチュエータの WEB サーバーのポートを指定します。 [default: 5001]
14 -n COUNT : n 回制御メッセージを受信したら終了します。0 は制限なし。 [default: 0]
15 -d : ダミーモードで実行します。
16 -D : デバッグモードで動作します。
17"""
19import atexit
20import logging
21import multiprocessing
22import os
23import pathlib
24import signal
25import threading
27import flask
28import flask_cors
29import my_lib.proc_util
30import my_lib.webapp.base
31import my_lib.webapp.config
32import my_lib.webapp.proxy
33import my_lib.webapp.util
35import unit_cooler.webui.webapi.cooler_stat
36import unit_cooler.webui.worker
38SCHEMA_CONFIG = "config.schema"
40# グローバル変数でワーカースレッドを管理
41worker_thread = None
44def term():
45 # ワーカーの終了フラグを設定
46 import unit_cooler.webui.worker
48 unit_cooler.webui.worker.term()
50 # ワーカースレッドの終了を待つ
51 if worker_thread and worker_thread.is_alive():
52 logging.info("Waiting for worker thread to finish...")
53 worker_thread.join(timeout=5)
54 if worker_thread.is_alive():
55 logging.warning("Worker thread did not finish in time")
57 # 子プロセスを終了
58 my_lib.proc_util.kill_child()
60 # プロセス終了
61 logging.info("Graceful shutdown completed")
62 os._exit(0)
65def signal_handler(signum, _frame):
66 """シグナルハンドラー: CTRL-Cや終了シグナルを受け取った際の処理"""
67 logging.info("Received signal %d, shutting down gracefully...", signum)
69 term()
72def create_app(config, arg):
73 setting = {
74 "control_host": "localhost",
75 "pub_port": 2222,
76 "actuator_host": "localhost",
77 "log_port": 5001,
78 "dummy_mode": False,
79 "msg_count": 0,
80 }
82 setting.update(arg)
84 logging.info("Using ZMQ server of %s:%d", setting["control_host"], setting["pub_port"])
86 my_lib.webapp.config.URL_PREFIX = "/unit-cooler"
87 my_lib.webapp.config.init(config["webui"])
89 message_queue = multiprocessing.Manager().Queue(10)
90 global worker_thread # noqa: PLW0603
91 worker_thread = threading.Thread(
92 target=unit_cooler.webui.worker.subscribe_worker,
93 args=(
94 config,
95 setting["control_host"],
96 setting["pub_port"],
97 message_queue,
98 pathlib.Path(config["webui"]["subscribe"]["liveness"]["file"]),
99 setting["msg_count"],
100 ),
101 )
102 worker_thread.start()
104 # NOTE: アクセスログは無効にする
105 logging.getLogger("werkzeug").setLevel(logging.ERROR)
107 app = flask.Flask("unit-cooler-webui")
109 # NOTE: アクセスログは無効にする
110 logging.getLogger("werkzeug").setLevel(logging.ERROR)
112 if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
113 if setting["dummy_mode"]:
114 logging.warning("Set dummy mode")
115 # NOTE: オプションでダミーモードが指定された場合、環境変数もそれに揃えておく
116 os.environ["DUMMY_MODE"] = "true"
117 else: # pragma: no cover
118 pass
120 def notify_terminate(): # pragma: no cover
121 term()
122 my_lib.webapp.log.info("🏃 アプリを再起動します。")
123 my_lib.webapp.log.term()
125 atexit.register(notify_terminate)
126 else: # pragma: no cover
127 pass
129 flask_cors.CORS(app)
131 app.config["CONFIG"] = config
132 app.config["MESSAGE_QUEUE"] = message_queue
134 app.json.compat = True
136 # Initialize proxy before registering blueprint
137 api_base_url = f"http://{setting['actuator_host']}:{setting['log_port']}/unit-cooler"
138 my_lib.webapp.proxy.init(api_base_url)
140 app.register_blueprint(my_lib.webapp.base.blueprint_default)
141 app.register_blueprint(my_lib.webapp.base.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX)
142 app.register_blueprint(my_lib.webapp.proxy.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX)
143 app.register_blueprint(my_lib.webapp.util.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX)
144 app.register_blueprint(
145 unit_cooler.webui.webapi.cooler_stat.blueprint, url_prefix=my_lib.webapp.config.URL_PREFIX
146 )
148 my_lib.webapp.config.show_handler_list(app)
150 unit_cooler.webui.webapi.cooler_stat.init(api_base_url)
152 # app.debug = True
154 return app
157if __name__ == "__main__":
158 import docopt
159 import my_lib.config
160 import my_lib.logger
162 args = docopt.docopt(__doc__)
164 config_file = args["-c"]
165 control_host = os.environ.get("HEMS_CONTROL_HOST", args["-s"])
166 pub_port = int(os.environ.get("HEMS_PUB_PORT", args["-p"]))
167 actuator_host = os.environ.get("HEMS_ACTUATOR_HOST", args["-a"])
168 log_port = int(os.environ.get("HEMS_LOG_PORT", args["-l"]))
169 dummy_mode = args["-d"]
170 msg_count = int(args["-n"])
171 debug_mode = args["-D"]
173 my_lib.logger.init("hems.unit_cooler", level=logging.DEBUG if debug_mode else logging.INFO)
175 config = my_lib.config.load(config_file, pathlib.Path(SCHEMA_CONFIG))
177 app = create_app(
178 config,
179 {
180 "control_host": control_host,
181 "pub_port": pub_port,
182 "actuator_host": actuator_host,
183 "log_port": log_port,
184 "dummy_mode": dummy_mode,
185 "msg_count": msg_count,
186 },
187 )
189 # Flaskアプリケーションを実行
190 try:
191 # NOTE: スクリプトの自動リロード停止したい場合は use_reloader=False にする
192 app.run(host="0.0.0.0", threaded=True, use_reloader=True, port=config["webui"]["webapp"]["port"]) # noqa: S104
193 except KeyboardInterrupt:
194 logging.info("Received KeyboardInterrupt, shutting down...")
195 signal_handler(signal.SIGINT, None)
196 finally:
197 term()