How to control a Tapo Smart Plug via Moonraker
👎 There's no proper support for Tapo smart plugs in Moonraker, the only solution I've seen wanted me to install Home Assistant which feels a bit much.
👎 There is a Python library for controlling Tapo smart plugs which is extremely easy to use and works well, however somehow Moonraker does not allow running arbitrary script on its host, or I couldn't find a way to do it, which is quite bizarre.
👎 Klipper can run macros, but these only run if the MCU is connected, which requires the power to be on in the first place.
🍀 Luckily the power
section of Moonraker's config allows arbitrary http requests (even if type: http
is, confusingly, not explicitly called out as supported in the documentation), a Python script with a tiny HTTP server attached can be used to control the smart plug, creating the following abomination:
Install the Python package "PyP100", specifically this fork https://github.com/almottier/TapoP100 because https://pypi.org/project/PyP100/ is unmaintained and does not work anymore due to changes to the Tapo authentication mechanism.
pip3 install git+https://github.com/almottier/TapoP100.git@main
/home/pi/tapo/server.py
, then edit the TAPO_ADDRESS
, TAPO_USERNAME
, TAPO_PASSWORD
fields appropriately.⚠ Note: this is the code for the P110
, you'll probably need to read the PyP100 docs and change a couple of lines if your plug is not the same.
#!/usr/bin/python3
import json
import signal
from http.server import SimpleHTTPRequestHandler, HTTPServer
from PyP100 import PyP110
TAPO_ADDRESS = "192.168.x.x"
TAPO_USERNAME = "your.email@address"
TAPO_PASSWORD = "hunter2"
p100 = PyP110.P110(TAPO_ADDRESS, TAPO_USERNAME, TAPO_PASSWORD)
p100.handshake()
p100.login()
running = True
def exit_gracefully(*args, **kwargs):
print("Terminating..")
global running
running = False
signal.signal(signal.SIGINT, exit_gracefully)
signal.signal(signal.SIGTERM, exit_gracefully)
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
global p100 # hack to fight Tapo session expiry, somehow just redoing handhsake and login doesn't work
try:
if self.path == "/on":
p100.turnOn()
elif self.path == "/off":
p100.turnOff()
self.wfile.write(json.dumps(p100.getDeviceInfo()).encode("utf-8"))
except Exception as e: # YOLO
p100 = PyP110.P110(TAPO_ADDRESS, TAPO_USERNAME, TAPO_PASSWORD)
p100.handshake()
p100.login()
if self.path == "/on":
p100.turnOn()
elif self.path == "/off":
p100.turnOff()
self.wfile.write(json.dumps(p100.getDeviceInfo()).encode("utf-8"))
return
if __name__ == '__main__':
server_class = HTTPServer
handler_class = MyHttpRequestHandler
server_address = ('127.0.0.1', 56427)
httpd = server_class(server_address, handler_class)
# intentionally making it slow, it doesn't need to react quickly
httpd.timeout = 1 # seconds
try:
while running:
httpd.handle_request()
except KeyboardInterrupt:
pass
chmod +x /home/pi/tapo/server.py
.Make the script autostart, create a service with your editor of choice, e.g. sudo nano /etc/systemd/system/tapo.service
[Unit]
Description=Tapo HTTP server
Wants=network.target
After=network.target
[Service]
User=pi
Group=pi
ExecStartPre=/bin/sleep 10
ExecStart=/home/pi/tapo/server.py
Restart=always
[Install]
WantedBy=multi-user.target
service tapo start
, if something goes wrong you can check status service tapo status
and logs journalctl -u tapo
. Then enable it so it autostarts systemctl enable tapo.service
Moonraker.cfg
, add this at the end (and maybe customise the device name just after "power"):
[power printer]
type: http
on_url: http://localhost:56427/on
off_url: http://localhost:56427/off
status_url: http://localhost:56427/
response_template:
{% set resp = http_request.last_response().json() %}
{% if resp["device_on"] %}
{"on"}
{% else %}
{"off"}
{% endif %}
bound_services: klipper
Optional goodies for Moonraker.cfg
, to add below [power printer]
off_when_shutdown: True
locked_while_printing: False
restart_klipper_when_powered: True
on_when_job_queued: True
Optional Klipper auto power off, courtesy of https://github.com/Arksine/moonraker/issues/167#issuecomment-1094223802
Add to Printer.cfg
or any Klipper config, adjust device name if needed:
[idle_timeout]
timeout: 600
gcode:
MACHINE_IDLE_TIMEOUT
# Turn on PSU
[gcode_macro M80]
gcode:
# Moonraker action
{action_call_remote_method('set_device_power',
device='printer',
state='on')}
# Turn off PSU
[gcode_macro M81]
gcode:
# Moonraker action
{action_call_remote_method('set_device_power',
device='printer',
state='off')}
[gcode_macro MACHINE_IDLE_TIMEOUT]
gcode:
M84
TURN_OFF_HEATERS
M81