Improve AC widget responsiveness
This commit is contained in:
parent
a2314e981a
commit
9368287c12
1 changed files with 163 additions and 26 deletions
|
|
@ -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"}))
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"text": f"<span font_family='FiraCode Nerd Font Mono' rise='-1500' size='140%'></span> {current:.0f} - ",
|
||||
"class": "off",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if mode == "cool":
|
||||
icon = ""
|
||||
css_class = "cool"
|
||||
elif mode == "heat":
|
||||
icon = ""
|
||||
css_class = "heat"
|
||||
else:
|
||||
icon = ""
|
||||
css_class = "other"
|
||||
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:.0f} - ",
|
||||
"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()
|
||||
attrs = state["attributes"]
|
||||
current_target = attrs["temperature"]
|
||||
if state:
|
||||
attrs = state["attributes"]
|
||||
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__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue