From ca40fd0d2faac6ab909de8fcaeb13a6698b8555f Mon Sep 17 00:00:00 2001 From: sillyfrog <816454+sillyfrog@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:21:13 +1000 Subject: [PATCH] Optional Helper scripts (#356) * Helper script support --------- Co-authored-by: Sillyfrog --- .gitignore | 9 + common.sh | 7 + common_run.sh | 2 + scripts/README.md | 66 ++++++++ scripts/configure-downlight.py-example | 219 +++++++++++++++++++++++++ scripts/post-flash.sh-example | 13 ++ scripts/pre-safety-checks.sh-example | 10 ++ scripts/pre-setup.sh-example | 8 + scripts/pre-wifi-config.sh-example | 23 +++ scripts/pre-wifi-exploit.sh-example | 12 ++ tuya-cloudcutter.sh | 6 + 11 files changed, 375 insertions(+) create mode 100644 scripts/README.md create mode 100644 scripts/configure-downlight.py-example create mode 100644 scripts/post-flash.sh-example create mode 100644 scripts/pre-safety-checks.sh-example create mode 100644 scripts/pre-setup.sh-example create mode 100644 scripts/pre-wifi-config.sh-example create mode 100644 scripts/pre-wifi-exploit.sh-example diff --git a/.gitignore b/.gitignore index 093e167..0f3ea90 100644 --- a/.gitignore +++ b/.gitignore @@ -151,3 +151,12 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ /actual-devices/ + + +# Don't add users sepecific scripts to the repo +scripts/*.sh +scripts/*.py + + + +.DS_Store diff --git a/common.sh b/common.sh index d0687b1..83397d4 100755 --- a/common.sh +++ b/common.sh @@ -23,6 +23,13 @@ if [ "${SUPPORTS_AP}" != "yes" ]; then read -n 1 -s -r -p "Press any key to continue, or CTRL+C to exit" fi +function run_helper_script () { + if [ -f "scripts/${1}.sh" ]; then + echo "Running helper script '${1}'" + source "scripts/${1}.sh" + fi +} + reset_nm () { if [ -z ${RESETNM+x} ]; then diff --git a/common_run.sh b/common_run.sh index e17cb6d..1de9904 100755 --- a/common_run.sh +++ b/common_run.sh @@ -56,6 +56,7 @@ echo "Long press the power/reset button on the device until it starts fast-blink echo "See https://support.tuya.com/en/help/_detail/K9hut3w10nby8 for more information." echo "================================================================================" echo "" +run_helper_script "pre-wifi-exploit" wifi_connect if [ ! $? -eq 0 ]; then echo "Failed to connect, please run this script again" @@ -90,6 +91,7 @@ echo "See https://support.tuya.com/en/help/_detail/K9hut3w10nby8 for more inform echo "================================================================================" echo "" sleep 5 +run_helper_script "pre-wifi-config" wifi_connect if [ ! $? -eq 0 ]; then echo "Failed to connect, please run this script again" diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..502801e --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,66 @@ +# Helper Scripts + +Scripts can be placed in this directory that will be run during the flashing process. + +This can be used to automate flashing or configure things specific for your environment. + +Scripts should be bash scripts, ending with a `.sh` extension, and have the execute bit set (`chmod +x *.sh`). If you want to call something that's not bash, call it from within the bash script (see `post-flash.sh-example` for an example). + +## Available Scripts + +The full list of available helper scripts is listed below. If a script is not included, it's skipped (ie: it's OK to just include the scripts you require). + +### pre-setup.sh + +If this script exists, it is called before the main script is run, after some initial basic checks have been performed. + +### pre-wifi-exploit.sh + +Called before the initial WiFi exploit (the first time the device is put into AP mode). + +### pre-wifi-config.sh + +Called before the device is configured to update to use the local server / get flashed (the second time the device is put into AP mode). + +### pre-safety-checks.sh + +Runs before the `tuya-cloudcutter` safety checks are performed before flashing custom firmware (configuring the local PC to be in AP mode). + +### post-flash.sh + +Called after the device has been successfully flashed. + +# Example scripts + +This directory includes a full set of example scripts to show what could be done. **These must be customized** for your specific use case and are not generic. + +The example scripts, if renamed to remove the `-example`, could be called with: + +``` +sudo MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power1 ./tuya-cloudcutter.sh +``` + +Or, for something more automatic, include the full configuration, eg: + +``` +sudo MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power1 ./tuya-cloudcutter.sh -f <3rd-party-firmware.bin> -p -r +``` + +The script could also be called from another script to flash several devices at once, eg: + +``` +#!/usr/bin/env bash + +# Flash 4 devices at once +echo Flashing via port 1 +MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power1 ./tuya-cloudcutter.sh -f <3rd-party-firmware.bin> -p -r + +echo Flashing via port 2 +MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power2 ./tuya-cloudcutter.sh -f <3rd-party-firmware.bin> -p -r + +echo Flashing via port 3 +MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power3 ./tuya-cloudcutter.sh -f <3rd-party-firmware.bin> -p -r + +echo Flashing via port 4 +MQTT_HOST=10.0.0.1 SWITCH_TOPIC=cmnd/flashing/power4 ./tuya-cloudcutter.sh -f <3rd-party-firmware.bin> -p -r +``` diff --git a/scripts/configure-downlight.py-example b/scripts/configure-downlight.py-example new file mode 100644 index 0000000..5d333b0 --- /dev/null +++ b/scripts/configure-downlight.py-example @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +""" +This example script configures an RGB downlight that has just been flashed with the +OpenBK7231N_App firmware. + +It was used on a Ubuntu 20.04 machine, but should be customized for your own use case. +Search this file for "here>" to find the places that need to be customized. + +See scripts/README.md for example usage + + +You will need to install requests and paho-mqtt: +pip install requests paho-mqtt +""" +import subprocess +import os +import time +import requests +import pathlib + +import paho.mqtt.client as mqtt + +AP_PREFIX = "OpenBK7231N_" + +MQTT_HOST = os.environ["MQTT_HOST"] +SWITCH_TOPIC = os.environ["SWITCH_TOPIC"] + + +mqtt_client = mqtt.Client() + + +COLOR_MAP = { + "1": "#2200000000", + "2": "#0022000000", + "3": "#0000220000", + "4": "#2200220000", +} + + +def get_wifi_adapter(): + # Get the wifi adapter name + # Return the adapter name + p = subprocess.run( + ["nmcli", "dev", "status"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + for l in p.stdout.splitlines(): + if " wifi " in l: + return l.split()[0] + raise ValueError("Failed to find wifi adapter") + + +def list_aps(): + # List the available APs + # Return a list of APs + + subprocess.run(["nmcli", "radio", "wifi", "off"]) + time.sleep(1) + subprocess.run(["nmcli", "radio", "wifi", "on"]) + time.sleep(1) + + p = subprocess.run( + [ + "nmcli", + "-t", + "-f", + "SSID,SECURITY", + "dev", + "wifi", + "list", + "--rescan", + "yes", + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + open_ssids = [] + for l in p.stdout.splitlines(): + ssid, security = l.split(":") + if security == "": + open_ssids.append(ssid) + return open_ssids + + +def find_ap(): + # Ensure the interface is in managed mode + subprocess.run(["nmcli", "device", "set", get_wifi_adapter(), "managed", "yes"]) + while 1: + print("Scanning for APs...") + for ap in list_aps(): + if ap.startswith(AP_PREFIX): + print(f"Found AP: {ap}") + return ap + time.sleep(3) + + +def connect_ap(ap): + # Connect to the AP + # Return True if successful, False otherwise + p = subprocess.run( + ["nmcli", "dev", "wifi", "connect", ap, "name", ap], + ) + if p.returncode != 0: + raise ValueError("Failed to connect to AP") + print("Connected to AP") + + +ID_PATH = pathlib.Path("light-id") + + +def get_id(): + if not ID_PATH.exists(): + ID_PATH.write_text("1") + return int(ID_PATH.read_text()) + 1 + + +def write_id(id): + ID_PATH.write_text(str(id)) + + +def make_req(url): + print("Making request: ", url) + RETRIES = 5 + for i in range(RETRIES): + try: + r = requests.get(url, timeout=5) + if not r.ok: + raise ValueError(f"Failed to make request: {url}") + print("Done") + time.sleep(1) + return + except Exception as e: + print(i + 1, "Error making request:", e) + time.sleep(3) + + raise ValueError(f"Failed to make request: {url} after {RETRIES} retries") + + +STATE = {"online": False} + + +def on_message(client, userdata, message): + print("Message received (IP Address): ", str(message.payload.decode("utf-8"))) + parts = message.topic.split("/") + msg = message.payload.decode("utf-8") + if parts[-1] == "ip": + print("Online") + STATE["online"] = True + + +def send_command(command, value): + mqtt_client.publish(f"cmnd/{device_name}/{command}", value) + time.sleep(1) + + +def main(): + global device_name + + print(f"MQTT_HOST: {MQTT_HOST}, SWITCH_TOPIC: {SWITCH_TOPIC}") + dev_id = get_id() + device_name = f"downlight{dev_id}" + print("Device name:", device_name) + # Find the AP to connect to + ap = find_ap() + # Connect to the AP + connect_ap(ap) + + # Configure the pins + make_req( + "http://192.168.4.1/cfg_pins?0=0&r0=0&1=0&r1=0&2=0&r2=0&3=0&r3=0&4=0&r4=0&5=0&r5=0&6=0&r6=0&7=23&r7=0&8=24&r8=0&9=0&r9=0&10=0&r10=0&11=0&r11=0&12=0&r12=0&13=0&r13=0&14=0&r14=0&15=0&r15=0&16=0&r16=0&17=0&r17=0&18=0&r18=0&19=0&r19=0&20=0&r20=0&21=0&r21=0&22=0&r22=0&23=0&r23=0&24=0&r24=0&25=0&r25=0&26=0&r26=0&27=0&r27=0&28=0&0=r28" + ) + # Configure MQTT + make_req( + f"http://192.168.4.1/cfg_mqtt_set?host=&port=1883&client={device_name}&group=dining&user=&password=" + ) + # Set default startup color + make_req( + "http://192.168.4.1/startup_command?data=backlog+led_basecolor_rgbcw+%230000007777%3B+led_enableAll+1%3B&startup_cmd=1" + ) + + make_req( + "http://192.168.4.1/cfg_wifi_set?ssid=&pass=" + ) + + mqtt_client.connect(MQTT_HOST) + mqtt_client.on_message = on_message + mqtt_client.loop_start() + mqtt_client.subscribe(f"{device_name}/ip") + + # Turn off the switch + print("Rebooting device...") + mqtt_client.publish(SWITCH_TOPIC, "off") + time.sleep(2) + mqtt_client.publish(SWITCH_TOPIC, "on") + + # Wait for the device to connect + print("Waiting for device to connect via MQTT...", end="", flush=True) + while not STATE["online"]: + time.sleep(1) + print(".", end="", flush=True) + print("Connected") + + output_color = COLOR_MAP.get(SWITCH_TOPIC[-1], "#2200000000") + + # Set the device name + send_command("ShortName", device_name) + send_command("FriendlyName", device_name) + send_command("led_basecolor_rgbcw", output_color) + send_command("power", "1") + + write_id(dev_id) + print(f"Flashed device {device_name} successfully") + + +if __name__ == "__main__": + main() diff --git a/scripts/post-flash.sh-example b/scripts/post-flash.sh-example new file mode 100644 index 0000000..70c0260 --- /dev/null +++ b/scripts/post-flash.sh-example @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# This runs after the device has been flashed +# This example connects to the new AP it creates, and configures the device using +# the configure-downlight.py script +# It then restores and restarts the network services, used on Ubuntu 20.04 + +python3 scripts/configure-downlight.py + + +sudo cp -ra /root/system-connections/ /etc/NetworkManager/ +sudo systemctl restart NetworkManager +sudo systemctl enable systemd-resolved.service +sudo systemctl start systemd-resolved.service \ No newline at end of file diff --git a/scripts/pre-safety-checks.sh-example b/scripts/pre-safety-checks.sh-example new file mode 100644 index 0000000..7090447 --- /dev/null +++ b/scripts/pre-safety-checks.sh-example @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# If this script exists, it is called before the local safety checks, including checking +# if there are local services running on required ports. + +# This example forcebily shuts down systemd-resolved, and keeps it down, removing the +# need to prompt the user to shut it down while running. +# Note if using this script, you will need to manually enable systemd-resolved.service +# after the device has been flashed. +# sudo systemctl disable systemd-resolved.service +sudo systemctl stop systemd-resolved.service \ No newline at end of file diff --git a/scripts/pre-setup.sh-example b/scripts/pre-setup.sh-example new file mode 100644 index 0000000..54e7651 --- /dev/null +++ b/scripts/pre-setup.sh-example @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# If this script exists, it is called before the main script is run, after some initial basic checks have been performed. + +# This example turns on the device that is connected to a smart switch via MQTT +# See pre-setup.sh-example for example usage + +# Turn on the device +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON \ No newline at end of file diff --git a/scripts/pre-wifi-config.sh-example b/scripts/pre-wifi-config.sh-example new file mode 100644 index 0000000..84c6dfe --- /dev/null +++ b/scripts/pre-wifi-config.sh-example @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# This runs before the second time we attempt to connect to the AP on the device +# This example uses a smart plug running via MQTT to put the device into AP mode automatically +# See scripts/README.md for example usage + +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF +sleep 5 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON +sleep 5 + +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON + +sleep 10 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON + diff --git a/scripts/pre-wifi-exploit.sh-example b/scripts/pre-wifi-exploit.sh-example new file mode 100644 index 0000000..4736f11 --- /dev/null +++ b/scripts/pre-wifi-exploit.sh-example @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# This runs before the first time we attempt to connect to the AP on the device +# This example uses a smart plug running via MQTT to put the device into AP mode automatically +# See scripts/README.md for example usage + +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON +sleep 5 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m OFF; sleep 1.4; mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON; sleep 1.4 +mosquitto_pub -h $MQTT_HOST -t $SWITCH_TOPIC -m ON \ No newline at end of file diff --git a/tuya-cloudcutter.sh b/tuya-cloudcutter.sh index dbe89ff..843138c 100755 --- a/tuya-cloudcutter.sh +++ b/tuya-cloudcutter.sh @@ -62,6 +62,8 @@ fi source common.sh +run_helper_script "pre-setup" + if [ ! $METHOD_DETACH ] && [ ! $METHOD_FLASH ]; then PS3="[?] Select your desired operation [1/2]: " select method in "Detach from the cloud and run Tuya firmware locally" "Flash 3rd Party Firmware"; do @@ -100,6 +102,8 @@ if [ $METHOD_FLASH ]; then fi source common_run.sh + +run_helper_script "pre-safety-checks" source safety_checks.sh if [ $METHOD_DETACH ]; then @@ -153,3 +157,5 @@ if [ $METHOD_FLASH ]; then exit 1 fi fi + +run_helper_script "post-flash" \ No newline at end of file