<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Dailyprompt – Yeri Tiete</title>
    <link>https://yeri.be/tag/dailyprompt/</link>
    <description>Yeri Tiete&#39;s blog</description>
    <language>en</language>
    <copyright>© Yeri Tiete</copyright>
    <lastBuildDate>Tue, 04 Jul 2023 15:20:06 +0200</lastBuildDate>
    <atom:link href="https://yeri.be/tag/dailyprompt/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>PoC: Betteruptime &#43; Python-kasa</title>
      <link>https://yeri.be/poc-betteruptime-python-kasa/</link>
      <pubDate>Tue, 04 Jul 2023 15:20:06 +0200</pubDate>
      <author>Yeri Tiete</author>
      <guid isPermaLink="true">https://yeri.be/poc-betteruptime-python-kasa/</guid><enclosure url="https://static.yeri.be/2023/07/kasa-smart-plug.jpg" length="0" type="image/jpeg" />
      <description>&lt;p&gt;&lt;strong&gt;Content Update&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;The provided scripts have been updated on 16 Jul 2023. Specifically the SmartStrip part was not working as intended. &lt;/p&gt;&#xA;&lt;hr class=&#34;wp-block-separator has-alpha-channel-opacity&#34;/&gt;&#xA;&lt;p&gt;I&#39;ve been a big fan of &lt;a rel=&#34;noreferrer noopener&#34; href=&#34;https://uptime.betterstack.com/&#34; target=&#34;_blank&#34;&gt;Betteruptime&lt;/a&gt;. I&#39;ve started using it to monitor all my assets online (websites, DNS, ping, successful script runs) as well as my servers (using &lt;a rel=&#34;noreferrer noopener&#34; href=&#34;https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/&#34; target=&#34;_blank&#34;&gt;heartbeats&lt;/a&gt;). &lt;/p&gt;&#xA;&lt;figure class=&#34;wp-block-image alignwide size-large&#34;&gt;&lt;a href=&#34;https://static.yeri.be/2023/07/mammoth.png&#34; target=&#34;_blank&#34; rel=&#34;noreferrer noopener&#34;&gt;&lt;img src=&#34;https://static.yeri.be/2023/07/mammoth-1024x570.png&#34; alt=&#34;Screenshot of Betteruptime showing a heartbeat that failed for several hours. &#34; class=&#34;wp-image-72880&#34;/&gt;&lt;/a&gt;&lt;figcaption class=&#34;wp-element-caption&#34;&gt;Image of a heartbeat that failed for several hours. After 2 hours of no hearbeat, it turned into an incident, and several hours later the heartbeats resumed. &lt;/figcaption&gt;&lt;/figure&gt;&#xA;&lt;p&gt;I have a few Raspberry Pi&#39;s, and once in a while they hang (not sure why, maybe USB-to-SSD issues or something). Nothing too critical, but annoying. &lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img src="https://static.yeri.be/2023/07/kasa-smart-plug.jpg" alt="PoC: Betteruptime + Python-kasa"></p><p><strong>Content Update</strong></p>
<p>The provided scripts have been updated on 16 Jul 2023. Specifically the SmartStrip part was not working as intended. </p>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<p>I've been a big fan of <a rel="noreferrer noopener" href="https://uptime.betterstack.com/" target="_blank">Betteruptime</a>. I've started using it to monitor all my assets online (websites, DNS, ping, successful script runs) as well as my servers (using <a rel="noreferrer noopener" href="https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/" target="_blank">heartbeats</a>). </p>
<figure class="wp-block-image alignwide size-large"><a href="https://static.yeri.be/2023/07/mammoth.png" target="_blank" rel="noreferrer noopener"><img src="https://static.yeri.be/2023/07/mammoth-1024x570.png" alt="Screenshot of Betteruptime showing a heartbeat that failed for several hours. " class="wp-image-72880"/></a><figcaption class="wp-element-caption">Image of a heartbeat that failed for several hours. After 2 hours of no hearbeat, it turned into an incident, and several hours later the heartbeats resumed. </figcaption></figure>
<p>I have a few Raspberry Pi's, and once in a while they hang (not sure why, maybe USB-to-SSD issues or something). Nothing too critical, but annoying. </p>
<p>I've plugged them all on <a href="https://www.tp-link.com/sg/home-networking/smart-plug/" target="_blank" rel="noreferrer noopener">TP-Link Kasa</a> smart plugs to remotely restart them if I had to (once or twice a year). </p>
<pre class="wp-block-verse">Note, to confuse everyone, TP-Link also launched Tapo, which... competes with Kasa and is not compatible, but does the exact same thing... ¯\_(ツ)_/¯</pre>
<p>After some <a rel="noreferrer noopener" href="https://medium.com/geekculture/use-raspberry-pi-and-tp-link-kasa-to-automate-your-devices-9f936a6243c1" target="_blank">Googling</a> (actually <a href="https://kagi.com" target="_blank" rel="noreferrer noopener">Kagi'ing</a>) I found out, there's a <a rel="noreferrer noopener" href="https://python-kasa.readthedocs.io/en/latest/" target="_blank">Python library</a> that lets you control your smart plugs. </p>
<p>So, the idea was born to:</p>
<ul>
<li>Check Betteruptime heartbeats, if down, power cycle the smart plug</li>
<li>Do this at most once per day (in case something else is causing issues)</li>
<li>Betteruptime <a rel="noreferrer noopener" href="https://betterstack.com/docs/uptime/api/get-a-single-hearbeat/" target="_blank">heartbeats</a> manage when a device is marked as offline (i.e.: it expects a heartbeat every 5 minutes, but will only consider the device down if no heartbeats are received for 2 hours).  </li>
<li>The bulk of the code had to be written by ChatGPT. Let ChatGPT choose the language (it ended up being a mix of Bash and Python)</li>
<li>Everything needs to run in Docker (using a cron, the Docker container doesn't daemonise)</li>
<li>These run on Raspberry Pi's (but of course the RPi can't check itself: so RPi1 checks for RPi2, and vice versa. As these RPis are on different networks (my parent's home, my own home, etc) I had to enable "<code>--net=host</code>" in <code>Docker run</code> to get the correct routes from the host system, but you may not actually need this</li>
<li>To top if off, sent an email (using Mailgun EU servers) to warn me something broke and it rebooted</li>
</ul>
<p>So, after some fiddling (half an evening or so) the proof-of-concept worked. </p>
<p>I should probably throw this in a Git repo but shrug. I don't want to give the impression that I'll maintain this and provide support.  </p>
<p>Dockerfile:</p>
<pre class="wp-block-code"><code>FROM python:alpine
RUN apk add bash curl jq
RUN pip3 install python-kasa
COPY heartbeat.sh kasa-api.py /
VOLUME /tmp/kasa/
CMD &#91;"/heartbeat.sh"]</code></pre>
<p>Python script <code>kasa-api.py</code> (this works with both <a href="https://www.tp-link.com/sg/home-networking/smart-plug/kp303/" target="_blank" rel="noreferrer noopener">smart strips</a> and <a href="https://www.kasasmart.com/us/products/smart-plugs/kasa-smart-wifi-plug-hs100" target="_blank" rel="noreferrer noopener">smart plugs</a>):</p>
<pre class="wp-block-code"><code>import sys
import asyncio
from kasa import SmartPlug, SmartStrip

async def main():
	if len(sys.argv) != 4:
		print("Usage: python kasa-api.py type IP-address outlet-index")
		return

	device_type = sys.argv&#91;1]
	ip_address = sys.argv&#91;2]
	outlet_index = int(sys.argv&#91;3])

	if device_type == "smartplug":
		await control_smart_plug(ip_address)
	elif device_type == "smartstrip":
		await control_smart_strip(ip_address, outlet_index)
	else:
		print(f"Unsupported device type: {device_type}")

async def control_smart_plug(ip_address):
	plug = SmartPlug(ip_address)

	try:
		await plug.update()

		# Retrieve the current state
		plug_state = plug.is_on

		# Turn off the plug
		await plug.turn_off()

		print(f"Turned off SmartPlug at {ip_address}")
		await asyncio.sleep(5)

		# Turn on the plug if it was previously on
		if plug_state:
			await plug.turn_on()

		print(f"Turned on SmartPlug at {ip_address}")
	except Exception as e:
		print(f"Failed to control SmartPlug at {ip_address}: {e}")

async def control_smart_strip(ip_address, outlet_index):
	strip = SmartStrip(ip_address)

	try:
		await strip.update()

		# Retrieve the current state of the specified child plug
		child_state = strip.children&#91;outlet_index].is_on

		# Turn off the specified child plug
		await strip.children&#91;outlet_index].turn_off()

		print(f"Turned off child plug {outlet_index} in SmartStrip at {ip_address}")
		await asyncio.sleep(5)

		# Turn on the child plug if it was previously on
		await strip.children&#91;outlet_index].turn_on()

		print(f"Turned on child plug {outlet_index} in SmartStrip at {ip_address}")
	except Exception as e:
		print(f"Failed to control SmartStrip at {ip_address}: {e}")

# Run the asyncio event loop
asyncio.run(main())</code></pre>
<p><code>heartbeat.sh</code> -- with example devices. Be sure to fill in the variables (including <code>hb</code>, that's the heartbeat ID you can get from the Betteruptime URL and the IP or DNS hostname of the smartplug):</p>
<pre class="wp-block-code"><code>#!/bin/bash

API_KEY="BetterUptime API token"
BU="https://uptime.betterstack.com/api/v2/heartbeats/" # no need to change this

MAILGUN_API_KEY="Mailgun API token"
MAILGUN_DOMAIN="mg.you.com" # use your own domain

if &#91;&#91; "$DEVICE" = tyr ]] || &#91;&#91; "$1" = tyr ]]; then

	# Tyr
	device="Tyr"
	hb=1111
	bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
	plug_type="smartplug"
	plug_host="smartplug1.kasa.you.com"

elif &#91;&#91; "$DEVICE" = mammoth ]] || &#91;&#91; "$1" = mammoth ]]; then

	# Mammoth
	device="mammoth"
	hb=2222
	bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
	plug_type="smartstrip"
	plug_host="smartstrip1.kasa.you.com"
	plug_index=0 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

elif &#91;&#91; "$DEVICE" = liana ]] || &#91;&#91; "$1" = liana ]]; then

	# Liana
	device="liana"
	hb=3333
	bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
	plug_type="smartstrip"
	plug_host="smartstrip1.kasa.you.com"
	plug_index=1 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

elif &#91;&#91; "$DEVICE" = eagle ]] || &#91;&#91; "$1" = eagle ]]; then

	device="eagle"
	hb=4444
	bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
	plug_type="smartstrip"
	plug_host="smartstrip1.kasa.yeri.be"
	plug_index=2 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

else
	echo "Unknown device."
	exit 1
fi

url=$BU/$hb

send_alert() {
	MAILGUN_URL="https://api.eu.mailgun.net/v3/$MAILGUN_DOMAIN/messages"
	from="kasa@you.com"
	to="alert@you.com"
	subject="Smartplug power cycled: $device"
	body="rebooted device $device!"$'\n'"Kasa IP: $plug_host."$'\n'"$bu"

	# Send alert email
	curl -s --user "api:$MAILGUN_API_KEY" \
		"$MAILGUN_URL" \
		-F from="$from" \
		-F to="$to" \
		-F subject="$subject" \
		-F text="$body"
}

kasa_cycle() {
	echo "Betteruptime heartbeat ($hb) says the service for $device is down, restarting."
	python /kasa-api.py "$plug_type" "$plug_host" "$plug_index"
	# Update the last execution date in the file
	echo "$current_date" &gt; "$file"
}

kasa_info() {
	kasa --host $plug_host
}

response=$(curl -sL "$url" -H "Authorization: Bearer $API_KEY")
status=$(echo "$response" | jq -r '.data.attributes.status')

if &#91;&#91; "$status" == "down" ]]; then
	dir="/tmp/.kasa/"
	mkdir -p "$dir"
	file="${dir}${device}.txt"

	# Get current date
	current_date=$(date "+%F")
	# Check if the file exists
	if &#91; -f "$file" ]; then
		# Get last execution date from the file
		last_execution=$(cat "$file")
		# We only want to run this once every 24hrs. If a reboot doesn't fix it, something more
		# serious is going on and likely needs manual intervention. No point spam rebooting the device.
		if &#91;&#91; "$current_date" != "$last_execution" ]]; then
			kasa_cycle
			send_alert
		else
			echo "Power cycle already executed today."
		fi
	else
		kasa_cycle
		send_alert
	fi
elif &#91;&#91; "$status" == "up" ]]; then
	echo "Betteruptime heartbeat says the service ($hb) for $device is up."
else
	# this could happen if the heartbeat is paused.
	echo "Unknown status."
	kasa_info
	exit 1
fi</code></pre>
<p>I run Docker with two scripts, a builder (<code>rebuild.sh</code>) and a file that runs it (<code>start.sh</code>). It should rebuild in case a docker cleanup script ran (and deleted dangling containers). </p>
<p>I run this as <code>root</code> and probably shouldn't, but yeah... That'll be for another lifetime. </p>
<p>Be sure to change the paths (<code>/root/git/kasa-api</code>) in both scripts. </p>
<p><code>rebuild.sh</code>:</p>
<pre class="wp-block-code"><code>#!/bin/bash
cd /root/git/kasa-api # the path where this project exists

git pull &gt; /dev/null

BASEIMAGE=`cat Dockerfile | grep FROM | awk '{print $2}'`
docker pull $BASEIMAGE
docker build -q -t kasa-api .
rm -f /tmp/.kasa/*.txt</code></pre>
<p><code>start.sh</code>:</p>
<pre class="wp-block-code"><code>#!/bin/bash

if &#91; -z "$1" ]; then
	echo "Missing device name."
	exit 1
fi

docker stop kasa-api 2&gt; /dev/null
docker rm kasa-api 2&gt; /dev/null

run_kasa() {
	DEVICE=$1
	docker run --net=host -v /tmp/.kasa:/tmp/.kasa --rm -e DEVICE=$DEVICE --name kasa-api kasa-api
}

if &#91;&#91; $(docker image ls | grep kasa-api) ]]; then
	run_kasa $1
else
	cd /root/git/kasa-api
	/root/git/kasa-api/rebuild.sh &gt; /dev/null
	run_kasa $1
fi</code></pre>
<p>And that's pretty much it. I run this using with cron in <code>/etc/cron.d/</code>. For example (be sure to edit the parameter/device name/path):</p>
<pre class="wp-block-code"><code>#
# cron-jobs for kasa-api
#

MAILTO=root

*/15 * * * *	root	if &#91; -x /root/git/kasa-api/start.sh ] &amp;&amp; &#91; -f /root/git/kasa-api/start.sh ]; then /root/git/kasa-api/start.sh tyr &gt;/dev/null; fi</code></pre>
<p>I'm sure there must be bugs in this ChatGPT generated code but... so far, it has actually worked.</p>
]]></content:encoded>
      <category>hardware</category><category>linux</category><category>networking</category><category>software</category>
      <category>bash</category><category>dailyprompt</category><category>dailyprompt-2001</category><category>docker</category><category>python</category>
    </item>
    
  </channel>
</rss>
