Skip to content

Instantly share code, notes, and snippets.

@HBIDamian
Created December 3, 2024 05:39
Show Gist options
  • Save HBIDamian/0404c5f6e00fceb5efad8368f38ba4b6 to your computer and use it in GitHub Desktop.
Save HBIDamian/0404c5f6e00fceb5efad8368f38ba4b6 to your computer and use it in GitHub Desktop.
Just a lil gui I made so I can see the status of my heater... πŸ˜…
# Author: HBIDamian
# Date: 2024-12-03
# Version: 1.0
# License: DBAD Public License
# DON'T BE A DICK PUBLIC LICENSE
# > Version 1.1, December 2016
# > Copyright (C) 2024 HBIDamian
# Everyone is permitted to copy and distribute verbatim or modified
# copies of this license document.
# > DON'T BE A DICK PUBLIC LICENSE
# > TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
# 1. Do whatever you like with the original work, just don't be a dick.
# Being a dick includes - but is not limited to - the following instances:
# 1a. Outright copyright infringement - Don't just copy this and change the name.
# 1b. Selling the unmodified original with no work done what-so-ever, that's REALLY being a dick.
# 1c. Modifying the original work to contain hidden harmful content. That would make you a PROPER dick.
# 2. If you become rich through modifications, related works/services, or supporting the original work,
# share the love. Only a dick would make loads off this work and not buy the original work's
# creator(s) a pint.
# 3. Code is provided with no warranty. Using somebody else's code and bitching when it goes wrong makes
# you a DONKEY dick. Fix the problem yourself. A non-dick would submit the fix back.
# NOTE:
# You will need your own account, private IP for the P110, and whatnot.
# It can be modified for other devices.
# Check out https://github.com/mihai-dinculescu/tapo/blob/main/tapo-py/examples for how.
import asyncio
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
from tapo import ApiClient
from tapo.requests import EnergyDataInterval
from datetime import datetime
import threading
class TapoControlApp:
def __init__(self, root):
self.root = root
self.root.title("Tapo Device (mah heater!) Control")
self.root.geometry("400x600")
self.root.resizable(False, False)
self.root.configure(bg="gray11")
self.root.wm_title("Tapo Device (mah heater!) Control")
# Initialize the client and device variables
self.client = ApiClient("[email protected]", "letMeInWithThisPassword")
self.device = None
# Set up the on/off button with an initial state of "off"
self.on_image = ImageTk.PhotoImage(Image.open("on_image.png").resize((100, 100)))
self.off_image = ImageTk.PhotoImage(Image.open("off_image.png").resize((100, 100)))
self.loading_image = ImageTk.PhotoImage(Image.open("loading_image.png").resize((100, 100)))
# Display initial loading image
self.button = tk.Button(self.root, image=self.loading_image, command=self.toggle_device, state=tk.DISABLED)
# remove the border
self.button.config(bg="gray11", highlightthickness=0, bd=0)
self.button.pack(pady=20)
# Labels for device information with word wrapping
# This shows A LOT of info... so do with it as you will. I'm leaving it out of my personal one.
# self.device_info_label = tk.Label(self.root, text="Device info: Waiting for data...", wraplength=350)
# self.device_info_label.pack(pady=5)
# Device info labels
self.current_power_label = tk.Label(self.root, text="Current power: Waiting for data...", wraplength=350)
# font color is white
self.current_power_label.config(bg="gray11", fg="white", font=("Arial", 18))
self.current_power_label.pack(pady=5)
self.device_usage_label = tk.Label(self.root, text="Device usage: Waiting for data...", wraplength=350)
self.device_usage_label.config(bg="gray11", fg="white", font=("Arial", 15))
self.device_usage_label.pack(pady=5)
self.energy_usage_label = tk.Label(self.root, text="Energy usage: Waiting for data...", wraplength=350)
self.energy_usage_label.config(bg="gray11", fg="white", font=("Arial", 15))
self.energy_usage_label.pack(pady=5)
# Start the async loop for fetching device data
self.schedule_fetch_device_data()
def schedule_fetch_device_data(self):
self.root.after(100, self.fetch_device_data_wrapper)
def fetch_device_data_wrapper(self):
asyncio.run_coroutine_threadsafe(self.fetch_device_data(), self.loop)
async def fetch_device_data(self):
try:
if self.device is None:
self.device = await self.client.p110("192.168.x.xxx")
# Fetch device data
device_info = await self.device.get_device_info()
device_usage = await self.device.get_device_usage()
current_power = await self.device.get_current_power()
energy_usage = await self.device.get_energy_usage()
today = datetime.today()
energy_data_hourly = await self.device.get_energy_data(EnergyDataInterval.Hourly, today)
energy_data_daily = await self.device.get_energy_data(EnergyDataInterval.Daily, datetime(today.year, 1, 1))
# Format device usage data
device_usage_text = (
f"Power Usage:\n"
f"Past 30 days: {device_usage.to_dict()['power_usage']['past30']} Wh\n"
f"Past 7 days: {device_usage.to_dict()['power_usage']['past7']} Wh\n"
f"Today: {device_usage.to_dict()['power_usage']['today']} Wh\n\n"
f"Time Usage (minutes):\n"
f"Past 30 days: {device_usage.to_dict()['time_usage']['past30']} min\n"
f"Past 7 days: {device_usage.to_dict()['time_usage']['past7']} min\n"
f"Today: {device_usage.to_dict()['time_usage']['today']} min"
)
# Format energy usage data
energy_usage_text = (
f"Current Power: {energy_usage.to_dict()['current_power']} mW\n"
f"Month Energy: {energy_usage.to_dict()['month_energy']} Wh\n"
f"Month Runtime: {energy_usage.to_dict()['month_runtime']} min\n"
f"Today Energy: {energy_usage.to_dict()['today_energy']} Wh\n"
f"Today Runtime: {energy_usage.to_dict()['today_runtime']} min"
)
# Update labels with the formatted text
self.current_power_label.config(text=f"Current Power: {current_power.to_dict()['current_power']} W")
self.device_usage_label.config(text=f"{device_usage_text}")
self.energy_usage_label.config(text=f"Energy Usage:\n{energy_usage_text}\n\n")
device_on = device_info.to_dict()['device_on'] # True or False
current_power_value = current_power.to_dict()['current_power']
if device_on:
self.button.config(image=self.on_image)
else:
self.button.config(image=self.off_image)
# Disable the button if the device is off and current power is greater than 0
if current_power_value > 0:
self.button.config(state=tk.DISABLED)
else:
self.button.config(state=tk.NORMAL)
# Enable the button now that the device is initialized
self.button.config(state=tk.NORMAL)
except Exception as e:
messagebox.showerror("Error", f"Failed to fetch device data: {e}")
# Refresh every 5 seconds by scheduling the next fetch
self.schedule_fetch_device_data()
def toggle_device(self):
if self.device is not None:
self.button.config(image=self.loading_image, state=tk.DISABLED)
self.schedule_toggle_device_async()
else:
messagebox.showerror("Error", "Device not initialized")
def schedule_toggle_device_async(self):
asyncio.run_coroutine_threadsafe(self.toggle_device_async(), self.loop)
async def toggle_device_async(self):
try:
current_power = await self.device.get_current_power()
current_power_value = current_power.to_dict()['current_power']
if current_power_value > 0:
await self.device.off()
self.button.config(image=self.off_image)
else:
await self.device.on()
self.button.config(image=self.on_image)
except Exception as e:
messagebox.showerror("Error", f"Failed to toggle device: {e}")
# After the toggle action is complete, re-enable the button
self.button.config(state=tk.NORMAL)
# Function to run the Tkinter main loop and asyncio event loop together
def run_gui():
root = tk.Tk()
app = TapoControlApp(root)
# Start asyncio loop in a separate thread
def start_asyncio_loop():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
app.loop = loop # Assign event loop to app instance
loop.run_forever()
# Create and start a new thread for the asyncio event loop
thread = threading.Thread(target=start_asyncio_loop, daemon=True)
thread.start()
# Start the Tkinter main loop
root.mainloop()
# Start the Tkinter GUI with asyncio loop integration
if __name__ == "__main__":
run_gui()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment