<?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>Python – Yeri Tiete</title><link>https://yeri.be/tag/python/</link><description>Yeri Tiete's blog</description><language>en</language><copyright>© Yeri Tiete</copyright><lastBuildDate>Fri, 06 Oct 2023 11:28:41 +0200</lastBuildDate><atom:link href="https://yeri.be/tag/python/index.xml" rel="self" type="application/rss+xml"/><item><title>IP Changed?</title><link>https://yeri.be/ip-changed/</link><pubDate>Fri, 06 Oct 2023 11:28:41 +0200</pubDate><author>Yeri Tiete</author><guid isPermaLink="true">https://yeri.be/ip-changed/</guid><description>&lt;p&gt;Very &lt;a href="https://gitlab.com/superuser.one/ipchanged" target="_blank" rel="noreferrer noopener"&gt;simple Python script&lt;/a&gt; that tracks one or multiple hosts/domains for IP changes, and prints it in a &lt;a href="https://matrix.org" target="_blank" rel="noreferrer noopener"&gt;Matrix&lt;/a&gt; room.&lt;/p&gt;
&lt;pre class="wp-block-code"&gt;&lt;code&gt;# docker logs ipchanged
29-09-2023 - 11:26:35 - Logged in as @turtlebot:matrix.org
29-09-2023 - 11:26:35 - IP address for be.yeri.be is 94.105.123.126
29-09-2023 - 11:26:35 - IP address for sg.yeri.be is 58.96.238.208
29-09-2023 - 11:26:35 - IP address for industry.yeri.be is 78.23.172.72
29-09-2023 - 11:29:36 - IP address for be.yeri.be is 94.105.123.126
29-09-2023 - 11:29:36 - IP address for sg.yeri.be is 58.96.238.208
29-09-2023 - 11:29:36 - IP address for industry.yeri.be is 78.23.172.72&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Includes a Docker container to keep it running. &lt;/p&gt;</description><content:encoded><![CDATA[<p>Very <a href="https://gitlab.com/superuser.one/ipchanged" target="_blank" rel="noreferrer noopener">simple Python script</a> that tracks one or multiple hosts/domains for IP changes, and prints it in a <a href="https://matrix.org" target="_blank" rel="noreferrer noopener">Matrix</a> room.</p>
<pre class="wp-block-code"><code># docker logs ipchanged
29-09-2023 - 11:26:35 - Logged in as @turtlebot:matrix.org
29-09-2023 - 11:26:35 - IP address for be.yeri.be is 94.105.123.126
29-09-2023 - 11:26:35 - IP address for sg.yeri.be is 58.96.238.208
29-09-2023 - 11:26:35 - IP address for industry.yeri.be is 78.23.172.72
29-09-2023 - 11:29:36 - IP address for be.yeri.be is 94.105.123.126
29-09-2023 - 11:29:36 - IP address for sg.yeri.be is 58.96.238.208
29-09-2023 - 11:29:36 - IP address for industry.yeri.be is 78.23.172.72</code></pre>
<p>Includes a Docker container to keep it running. </p>
<figure class="wp-block-image size-large"><img src="https://static.yeri.be/2023/09/Screenshot-2023-09-29-at-11.31.00-645x1024.png" alt="" class="wp-image-73549"/></figure>
<p>I made it less noisy (i.e. won't talk when the IP didn't change) and as the IP of my DynDNS hosts hasn't changed yet, there's not much to see... ;) </p>
]]></content:encoded><category>Networking</category><category>Software</category><category>python</category></item><item><title>Check websites with LanguageTool for typos</title><link>https://yeri.be/check-websites-with-languagetool-for-typos/</link><pubDate>Fri, 08 Sep 2023 17:40:29 +0200</pubDate><author>Yeri Tiete</author><guid isPermaLink="true">https://yeri.be/check-websites-with-languagetool-for-typos/</guid><description>&lt;p&gt;This is quick and dirty (and with the help of ChatGPT). &lt;/p&gt;
&lt;p&gt;&lt;a href="https://yeri.be/tag/flatturtle" data-type="link" data-id="https://flatturtle.com"&gt;FlatTurtle&lt;/a&gt; has a &lt;a href="https://flatturtle.com" data-type="link" data-id="https://flatturtle.com" target="_blank" rel="noreferrer noopener"&gt;new site&lt;/a&gt;, and there's been some fine-tuning here and there that led to a few typos creeping in. I wanted a quick tool to plug in a page, and that would highlight possible mistakes.&lt;/p&gt;
&lt;p&gt;I've been a personal (paying) user of &lt;a href="https://languagetool.org/" target="_blank" rel="noreferrer noopener"&gt;LanguageTool&lt;/a&gt; for a few years now (European, and less spammy and dodgy than Grammarly)&lt;/p&gt;</description><content:encoded><![CDATA[<p>This is quick and dirty (and with the help of ChatGPT). </p>
<p><a href="https://yeri.be/tag/flatturtle" data-type="link" data-id="https://flatturtle.com">FlatTurtle</a> has a <a href="https://flatturtle.com" data-type="link" data-id="https://flatturtle.com" target="_blank" rel="noreferrer noopener">new site</a>, and there's been some fine-tuning here and there that led to a few typos creeping in. I wanted a quick tool to plug in a page, and that would highlight possible mistakes.</p>
<p>I've been a personal (paying) user of <a href="https://languagetool.org/" target="_blank" rel="noreferrer noopener">LanguageTool</a> for a few years now (European, and less spammy and dodgy than Grammarly)</p>
<p>Started off with a <a rel="noreferrer noopener" href="https://gitlab.com/superuser.one/languagetool-website-checker/-/blob/38c4c09a560f70e549e2d7266ad11ea256d82383/typocheck.py" data-type="link" data-id="https://gitlab.com/superuser.one/languagetool-website-checker/-/blob/38c4c09a560f70e549e2d7266ad11ea256d82383/typocheck.py" target="_blank">terminal tool</a>, but in the end that wasn't working out (hard to get the colouring to work and make it clear enough). </p>
<p>Figured a website would be easier:</p>
<ul>
<li>Insert a site</li>
<li>Let it go through the LanguageTool API for mistakes*</li>
<li>Show what is potentially wrong and explain why so I can go and edit it</li>
</ul>
<p>(*) Surprisingly hard because it needs to trim all HTML and js and other crap. And it has issues detecting headers (without punctuation) from paragraph text, etc). </p>
<p>It's far from perfect, but it works well enough for half a day of fiddling around. </p>
<figure class="wp-block-image alignwide size-large"><a href="https://static.yeri.be/2023/09/typo-checker-ft.png"><img src="https://static.yeri.be/2023/09/typo-checker-ft-1024x680.png" alt="" class="wp-image-73287"/></a></figure>
<p>You can hover your mouse over the red words to get some information as to why something is wrong.</p>
<p>The code, provided as-is, <a rel="noreferrer noopener" href="https://gitlab.com/superuser.one/languagetool-website-checker" data-type="link" data-id="https://gitlab.com/superuser.one/languagetool-website-checker" target="_blank">is here</a>, and you can run it using:</p>
<pre class="wp-block-code"><code>python3 -m pip install flask selenium beautifulsoup4 geckodriver-autoinstaller requests
python3 web_check.py --api-key KEY --username EMAIL</code></pre>
<p>And opening http://localhost:5000.</p>
<p>EMAIL is your login, the KEY can be found <a rel="noreferrer noopener" href="https://languagetool.org/editor/settings/access-tokens" target="_blank">here</a>.</p>
<p>Have fun. </p>
]]></content:encoded><category>Software</category><category>www</category><category>AI</category><category>flatturtle</category><category>mac os x</category><category>python</category></item><item><title>PoC: Betteruptime + 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><description>&lt;p&gt;&lt;strong&gt;Content Update&lt;/strong&gt;&lt;/p&gt;
&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;
&lt;hr class="wp-block-separator has-alpha-channel-opacity"/&gt;
&lt;p&gt;I've been a big fan of &lt;a rel="noreferrer noopener" href="https://uptime.betterstack.com/" target="_blank"&gt;Betteruptime&lt;/a&gt;. I'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="noreferrer noopener" href="https://betterstack.com/docs/uptime/cron-and-heartbeat-monitor/" target="_blank"&gt;heartbeats&lt;/a&gt;). &lt;/p&gt;
&lt;figure class="wp-block-image alignwide size-large"&gt;&lt;a href="https://static.yeri.be/2023/07/mammoth.png" target="_blank" rel="noreferrer noopener"&gt;&lt;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"/&gt;&lt;/a&gt;&lt;figcaption class="wp-element-caption"&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;
&lt;p&gt;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. &lt;/p&gt;</description><content:encoded><![CDATA[<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>