Created
December 3, 2024 05:39
-
-
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... π
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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