Improve AC widget responsiveness

This commit is contained in:
ltadeu6 2026-04-01 14:55:16 -03:00
parent a2314e981a
commit 9368287c12

View file

@ -1,9 +1,12 @@
#!/run/current-system/sw/bin/python3
import requests
import sys
import os
import json
import time
import subprocess
import urllib.request
import urllib.error
HA_URL = "http://localhost:8123"
ENTITY = "climate.ar"
@ -12,6 +15,10 @@ TOKEN_FILE = os.path.expanduser("~/.config/secrets/ha_token")
STEP = 1
MIN_TEMP = 18
MAX_TEMP = 30
PENDING_FILE = os.path.expanduser("~/.cache/waybar_air_pending.json")
SEND_DELAY = 3
PENDING_TTL = 15
CONFIRM_TTL = 20
def get_token():
@ -23,52 +30,146 @@ HEADERS = {"Authorization": f"Bearer {get_token()}", "Content-Type": "applicatio
def get_state():
r = requests.get(f"{HA_URL}/api/states/{ENTITY}", headers=HEADERS)
return r.json()
return request_json("GET", f"/api/states/{ENTITY}")
def set_temperature(temp):
requests.post(
f"{HA_URL}/api/services/climate/set_temperature",
headers=HEADERS,
json={"entity_id": ENTITY, "temperature": temp},
request_json(
"POST",
"/api/services/climate/set_temperature",
{"entity_id": ENTITY, "temperature": temp},
)
def set_mode(mode):
requests.post(
f"{HA_URL}/api/services/climate/set_hvac_mode",
headers=HEADERS,
json={"entity_id": ENTITY, "hvac_mode": mode},
request_json(
"POST",
"/api/services/climate/set_hvac_mode",
{"entity_id": ENTITY, "hvac_mode": mode},
)
def request_json(method, path, payload=None):
url = f"{HA_URL}{path}"
data = None
headers = dict(HEADERS)
if payload is not None:
data = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
body = resp.read()
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError):
return None
if not body:
return None
try:
return json.loads(body)
except json.JSONDecodeError:
return None
def read_pending():
try:
with open(PENDING_FILE) as f:
data = json.load(f)
temp = data.get("temperature")
ts = data.get("ts")
sent = data.get("sent", False)
sent_ts = data.get("sent_ts")
if isinstance(temp, (int, float)) and isinstance(ts, (int, float)):
sent_ts_val = None
if isinstance(sent_ts, (int, float)):
sent_ts_val = float(sent_ts)
return {
"temperature": float(temp),
"ts": float(ts),
"sent": bool(sent),
"sent_ts": sent_ts_val,
}
except FileNotFoundError:
return None
except (OSError, ValueError, json.JSONDecodeError):
return None
return None
def write_pending(temp, sent=False, sent_ts=None, ts=None):
os.makedirs(os.path.dirname(PENDING_FILE), exist_ok=True)
if ts is None:
ts = time.time()
payload = {"temperature": float(temp), "ts": float(ts), "sent": bool(sent)}
if sent_ts is not None:
payload["sent_ts"] = float(sent_ts)
with open(PENDING_FILE, "w") as f:
json.dump(payload, f)
def clear_pending():
try:
os.remove(PENDING_FILE)
except FileNotFoundError:
pass
except OSError:
pass
def display():
state = get_state()
if not state:
print(json.dumps({"text": "-- - ", "class": "off"}))
return
mode = state["state"]
attrs = state["attributes"]
current = attrs["current_temperature"]
current = attrs.get("current_temperature")
target = attrs.get("temperature")
pending = read_pending()
if pending is not None:
pending_temp = pending["temperature"]
pending_ts = pending["ts"]
now = time.time()
show_pending = now - pending_ts <= PENDING_TTL
if pending["sent"] and pending.get("sent_ts") is not None:
show_pending = show_pending or (now - pending["sent_ts"] <= CONFIRM_TTL)
if target == pending_temp:
clear_pending()
elif show_pending:
target = pending_temp
else:
clear_pending()
if current is None:
current = 0
if mode == "off":
print(json.dumps({"text": f"{current:.0f} - ", "class": "off"}))
return
if mode == "cool":
icon = ""
css_class = "cool"
elif mode == "heat":
icon = ""
css_class = "heat"
else:
icon = ""
css_class = "other"
print(
json.dumps(
{
"text": f"<span font_family='FiraCode Nerd Font Mono' rise='-1500' size='140%'>{icon}</span> {target:.0f} - ",
"text": f"<span font_family='FiraCode Nerd Font Mono' rise='-1500' size='140%'></span> {current:.0f} - ",
"class": "off",
}
)
)
return
mode_map = {
"cool": ("", "cool"),
"heat": ("", "heat"),
"dry": ("", "dry"),
"fan_only": ("󰈐", "fan"),
}
icon, css_class = mode_map.get(mode, ("", "other"))
if target is None:
target_text = f"{current:.0f}"
else:
target_text = f"{target:.0f}"
print(
json.dumps(
{
"text": f"<span font_family='FiraCode Nerd Font Mono' rise='-1500' size='140%'>{icon}</span> {target_text} - ",
"class": css_class,
}
)
@ -77,11 +178,45 @@ def display():
def change(delta):
state = get_state()
if state:
attrs = state["attributes"]
current_target = attrs["temperature"]
current_target = attrs.get("temperature")
if current_target is None:
current_target = attrs.get("current_temperature")
else:
current_target = None
pending = read_pending()
if pending is not None:
current_target = pending["temperature"]
if current_target is None:
return
new_temp = max(MIN_TEMP, min(MAX_TEMP, current_target + delta))
set_temperature(new_temp)
write_pending(new_temp, sent=False)
subprocess.Popen(
[sys.executable, os.path.abspath(__file__), "flush"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def flush_pending():
pending = read_pending()
if pending is None:
return
ts = pending["ts"]
remaining = SEND_DELAY - (time.time() - ts)
if remaining > 0:
time.sleep(remaining)
pending = read_pending()
if pending is None:
return
temp2 = pending["temperature"]
ts2 = pending["ts"]
if ts2 != ts:
return
set_temperature(temp2)
write_pending(temp2, sent=True, sent_ts=time.time(), ts=ts2)
def main():
@ -101,6 +236,8 @@ def main():
change(STEP)
elif cmd == "down":
change(-STEP)
elif cmd == "flush":
flush_pending()
if __name__ == "__main__":