305 lines
10 KiB
Python
305 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
AC Control System untuk AC Panasonic
|
|
Mengontrol AC berdasarkan deteksi kehadiran (AUTO) atau manual via MQTT
|
|
"""
|
|
|
|
import paho.mqtt.client as mqtt
|
|
from paho.mqtt import client as mqtt_client
|
|
import subprocess
|
|
import os
|
|
import json
|
|
import time
|
|
import signal
|
|
import sys
|
|
import threading
|
|
from datetime import datetime
|
|
|
|
# ====================== KONFIGURASI ======================
|
|
MQTT_BROKER = "localhost"
|
|
MQTT_PORT = 1883
|
|
CONTROL_TOPIC = "classroom/ac/control"
|
|
STATUS_TOPIC = "classroom/ac/status"
|
|
PRESENCE_TOPIC = "classroom/presence"
|
|
|
|
# GPIO untuk IR
|
|
TRANSMITTER_GPIO = 18
|
|
|
|
# File konfigurasi
|
|
IRRP_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "irrp.py")
|
|
JSON_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ac_codes.json")
|
|
LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ac_control.log")
|
|
|
|
# State variables
|
|
AUTO_MODE = True
|
|
CURRENT_AC_STATE = "off"
|
|
|
|
# Timer Delay
|
|
DELAY_TIMER_MINUTES = 5
|
|
DELAY_TIMER_ACTIVE = False
|
|
DELAY_TIMER_THREAD = None
|
|
|
|
# ====================== LOGGING ======================
|
|
def log_message(message, level="INFO"):
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
log_entry = f"[{timestamp}] [{level}] {message}"
|
|
print(log_entry)
|
|
try:
|
|
with open(LOG_FILE, "a") as f:
|
|
f.write(log_entry + "\n")
|
|
except:
|
|
pass
|
|
|
|
# ====================== FUNGSI KIRIM STATUS ======================
|
|
def publish_status(client, reason=""):
|
|
"""Kirim status AC ke MQTT"""
|
|
status_message = {
|
|
"ac_state": CURRENT_AC_STATE,
|
|
"auto_mode": AUTO_MODE,
|
|
"delay_active": DELAY_TIMER_ACTIVE,
|
|
"delay_remaining": DELAY_TIMER_MINUTES * 60 if DELAY_TIMER_ACTIVE else 0,
|
|
"reason": reason,
|
|
"timestamp": datetime.now().isoformat()
|
|
}
|
|
client.publish(STATUS_TOPIC, json.dumps(status_message))
|
|
log_message(f"📤 Status dikirim: {CURRENT_AC_STATE} (reason: {reason})")
|
|
|
|
# ====================== IR CONTROL ======================
|
|
def send_ir(key):
|
|
log_message(f"Mengirim IR: {key}")
|
|
|
|
if not os.path.exists(JSON_FILE):
|
|
log_message(f"File {JSON_FILE} tidak ditemukan!", "ERROR")
|
|
return False
|
|
|
|
try:
|
|
with open(JSON_FILE, 'r') as f:
|
|
codes = json.load(f)
|
|
if key not in codes:
|
|
log_message(f"Key '{key}' tidak ditemukan di JSON", "ERROR")
|
|
return False
|
|
except Exception as e:
|
|
log_message(f"Gagal baca JSON: {e}", "ERROR")
|
|
return False
|
|
|
|
cmd = ["python3", IRRP_SCRIPT, "-p", "-g", str(TRANSMITTER_GPIO), "-f", JSON_FILE, key]
|
|
|
|
try:
|
|
subprocess.run(cmd, check=True, capture_output=True, text=True, timeout=10)
|
|
log_message(f"IR berhasil dikirim: {key}")
|
|
return True
|
|
except Exception as e:
|
|
log_message(f"Gagal kirim IR: {e}", "ERROR")
|
|
return False
|
|
|
|
def set_ac_state(state, client=None, reason=""):
|
|
global CURRENT_AC_STATE
|
|
|
|
if state == "on" and CURRENT_AC_STATE == "off":
|
|
log_message("Menyalakan AC...")
|
|
if send_ir("POWER_ON"):
|
|
CURRENT_AC_STATE = "on"
|
|
if client:
|
|
publish_status(client, reason or "power_on")
|
|
return True
|
|
return False
|
|
|
|
elif state == "off" and CURRENT_AC_STATE == "on":
|
|
log_message("Mematikan AC...")
|
|
if send_ir("POWER_OFF"):
|
|
CURRENT_AC_STATE = "off"
|
|
if client:
|
|
publish_status(client, reason or "power_off")
|
|
return True
|
|
return False
|
|
|
|
return False
|
|
|
|
# ====================== DELAY TIMER ======================
|
|
def start_delay_timer(client):
|
|
global DELAY_TIMER_ACTIVE, DELAY_TIMER_THREAD
|
|
|
|
if DELAY_TIMER_ACTIVE:
|
|
log_message("Delay timer sudah aktif")
|
|
return
|
|
|
|
DELAY_TIMER_ACTIVE = True
|
|
log_message(f"⏳ Delay timer dimulai: {DELAY_TIMER_MINUTES} menit")
|
|
|
|
# Kirim status timer aktif
|
|
publish_status(client, "delay_started")
|
|
|
|
def timer_worker():
|
|
global DELAY_TIMER_ACTIVE, CURRENT_AC_STATE
|
|
total_seconds = DELAY_TIMER_MINUTES * 60
|
|
|
|
for remaining in range(total_seconds, -1, -1):
|
|
if not DELAY_TIMER_ACTIVE:
|
|
log_message("Delay timer dibatalkan")
|
|
return
|
|
|
|
if remaining > 0:
|
|
# Update setiap 10 detik
|
|
if remaining % 10 == 0 or remaining <= 5:
|
|
status_msg = {
|
|
"ac_state": CURRENT_AC_STATE,
|
|
"auto_mode": AUTO_MODE,
|
|
"delay_active": True,
|
|
"delay_remaining": remaining
|
|
}
|
|
client.publish(STATUS_TOPIC, json.dumps(status_msg))
|
|
|
|
minutes = remaining // 60
|
|
seconds = remaining % 60
|
|
log_message(f"Sisa waktu: {minutes:02d}:{seconds:02d}")
|
|
|
|
time.sleep(1)
|
|
else:
|
|
# Timer habis
|
|
if DELAY_TIMER_ACTIVE and CURRENT_AC_STATE == "on":
|
|
log_message("⏰ TIMER HABIS - Mematikan AC")
|
|
if send_ir("POWER_OFF"):
|
|
CURRENT_AC_STATE = "off"
|
|
publish_status(client, "delay_timer_expired")
|
|
DELAY_TIMER_ACTIVE = False
|
|
return
|
|
|
|
DELAY_TIMER_THREAD = threading.Thread(target=timer_worker, daemon=True)
|
|
DELAY_TIMER_THREAD.start()
|
|
|
|
def cancel_delay_timer(client):
|
|
global DELAY_TIMER_ACTIVE
|
|
if DELAY_TIMER_ACTIVE:
|
|
DELAY_TIMER_ACTIVE = False
|
|
log_message("Delay timer dibatalkan")
|
|
publish_status(client, "delay_cancelled")
|
|
|
|
# ====================== MQTT HANDLERS ======================
|
|
def on_connect(client, userdata, flags, reason_code, properties):
|
|
if reason_code == 0:
|
|
log_message("✅ MQTT terhubung ke broker")
|
|
client.subscribe([(CONTROL_TOPIC, 0), (PRESENCE_TOPIC, 0)])
|
|
log_message(f"Subscribed ke: {CONTROL_TOPIC} & {PRESENCE_TOPIC}")
|
|
|
|
# Kirim status awal
|
|
publish_status(client, "initial_connection")
|
|
else:
|
|
log_message(f"❌ MQTT connect gagal: {reason_code}", "ERROR")
|
|
|
|
def on_message(client, userdata, msg):
|
|
global AUTO_MODE, CURRENT_AC_STATE, DELAY_TIMER_MINUTES
|
|
|
|
topic = msg.topic
|
|
payload = msg.payload.decode().strip()
|
|
|
|
log_message(f"MQTT → {topic} : {payload}")
|
|
|
|
# ==================== PRESENCE (AUTO MODE) ====================
|
|
if topic == PRESENCE_TOPIC:
|
|
if not AUTO_MODE:
|
|
log_message("Mode MANUAL aktif → ignore presence")
|
|
return
|
|
|
|
presence = payload.lower()
|
|
|
|
if presence == "ada":
|
|
log_message("👥 Ada orang terdeteksi (AUTO)")
|
|
cancel_delay_timer(client)
|
|
|
|
if CURRENT_AC_STATE == "off":
|
|
log_message("🔛 Menyalakan AC otomatis...")
|
|
set_ac_state("on", client, "presence_detected")
|
|
|
|
elif presence == "tidak ada":
|
|
log_message("🚪 Tidak ada orang (AUTO)")
|
|
if CURRENT_AC_STATE == "on" and not DELAY_TIMER_ACTIVE:
|
|
log_message(f"⏳ Memulai delay timer {DELAY_TIMER_MINUTES} menit...")
|
|
start_delay_timer(client)
|
|
|
|
# ==================== CONTROL (DARI WEB) ====================
|
|
elif topic == CONTROL_TOPIC:
|
|
payload_lower = payload.lower()
|
|
|
|
if payload_lower == "auto_on":
|
|
AUTO_MODE = True
|
|
cancel_delay_timer(client)
|
|
log_message("🤖 Mode AUTO diaktifkan")
|
|
publish_status(client, "auto_mode_on")
|
|
|
|
elif payload_lower == "auto_off":
|
|
AUTO_MODE = False
|
|
cancel_delay_timer(client)
|
|
log_message("👤 Mode MANUAL diaktifkan")
|
|
publish_status(client, "manual_mode_on")
|
|
|
|
elif payload_lower.startswith("delay_"):
|
|
try:
|
|
minutes = int(payload_lower.split("_")[1])
|
|
if 1 <= minutes <= 60:
|
|
DELAY_TIMER_MINUTES = minutes
|
|
log_message(f"⚙️ Delay timer diubah menjadi {minutes} menit")
|
|
publish_status(client, "delay_changed")
|
|
except:
|
|
pass
|
|
|
|
elif payload_lower == "on":
|
|
log_message("👤 Manual: Menyalakan AC")
|
|
cancel_delay_timer(client)
|
|
set_ac_state("on", client, "manual_on")
|
|
|
|
elif payload_lower == "off":
|
|
log_message("👤 Manual: Mematikan AC")
|
|
cancel_delay_timer(client)
|
|
set_ac_state("off", client, "manual_off")
|
|
|
|
elif payload_lower == "status":
|
|
publish_status(client, "manual_request")
|
|
|
|
# ====================== SIGNAL HANDLER ======================
|
|
def signal_handler(sig, frame):
|
|
log_message("Menerima sinyal shutdown...")
|
|
sys.exit(0)
|
|
|
|
# ====================== MAIN ======================
|
|
if __name__ == "__main__":
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
signal.signal(signal.SIGTERM, signal_handler)
|
|
|
|
log_message("=" * 70)
|
|
log_message("🚀 AC CONTROL SYSTEM - PANASONIC")
|
|
log_message("=" * 70)
|
|
|
|
# Tampilkan kode IR yang tersedia
|
|
if os.path.exists(JSON_FILE):
|
|
try:
|
|
with open(JSON_FILE, 'r') as f:
|
|
codes = json.load(f)
|
|
log_message(f"Kode IR tersedia: {list(codes.keys())}")
|
|
except Exception as e:
|
|
log_message(f"Gagal baca ac_codes.json: {e}", "ERROR")
|
|
else:
|
|
log_message(f"⚠️ File {JSON_FILE} tidak ditemukan!", "WARNING")
|
|
log_message(" Jalankan record terlebih dahulu: python3 rcd.py POWER_ON")
|
|
|
|
# Status awal
|
|
log_message(f"Mode awal : {'AUTO' if AUTO_MODE else 'MANUAL'}")
|
|
log_message(f"AC : {CURRENT_AC_STATE}")
|
|
log_message(f"Delay Timer : {DELAY_TIMER_MINUTES} menit")
|
|
log_message(f"Log file : {LOG_FILE}")
|
|
|
|
# MQTT Client
|
|
try:
|
|
client = mqtt.Client(mqtt_client.CallbackAPIVersion.VERSION2)
|
|
client.on_connect = on_connect
|
|
client.on_message = on_message
|
|
client.connect(MQTT_BROKER, MQTT_PORT, 60)
|
|
|
|
log_message("\n✅ Sistem siap. Menunggu perintah MQTT...")
|
|
log_message(f" Subscribe ke: {CONTROL_TOPIC} & {PRESENCE_TOPIC}")
|
|
log_message(" Tekan Ctrl+C untuk berhenti\n")
|
|
|
|
client.loop_forever()
|
|
|
|
except Exception as e:
|
|
log_message(f"❌ Error MQTT: {e}", "ERROR")
|
|
sys.exit(1) |