diff --git a/.gitignore b/.gitignore index a6b7ba4..5e49b42 100644 --- a/.gitignore +++ b/.gitignore @@ -158,7 +158,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -bluewind -bluewind/* \ No newline at end of file +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 4e8e79e..0acd1e1 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,29 @@ Bluetooth control app for Wahoo Headwind. ## Usage -To run the flask app: +To run the flask app for development and testing: ```bash +git clone https://teapot.octopusx.de/accidentallycompetent/bluewind.git +cd bluewind +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install --upgrade pip +pip3 install -r requirements.txt export FLASK_ADDRESS='' -flask --app api run --host=0.0.0.0 +export FLASK_URL='
Failed to turn headwind on

", 503 - return "

Turning headwind on

", 200 - -@app.route("/sleep") -async def sleep(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.sleep() - if not fan_status: - return "

Failed to put headwind to sleep

", 503 - return "

Putting headwind to sleep

", 200 - -@app.route("/speed/") -async def speed(speed): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.manualSpeed(speed) - if not fan_status: - return f"

Failed to set headwind speed to {speed}

", 503 - return f"

Setting headwind speed to {speed}

", 200 - -@app.route("/hr") -async def hr(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.hr() - if not fan_status: - return "

Failed to set headwind to HR mode

", 503 - return "

Setting headwind to HR mode

", 200 - -@app.route("/off") -async def off(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.off() - if not fan_status: - return "

Failed to turn headwind off

", 503 - return "

Turning headwind off

", 200 \ No newline at end of file diff --git a/bluewind/__init__.py b/bluewind/__init__.py new file mode 100644 index 0000000..79e0781 --- /dev/null +++ b/bluewind/__init__.py @@ -0,0 +1,12 @@ +# Initialise flask +from flask import Flask +app = Flask(__name__) +app.config['SERVER_NAME'] = '0.0.0.0:5000' +app.config.from_prefixed_env() + +# Initialise the bluetooth stack +from bluewind import headwind +fan = headwind.Headwind(app, app.config["ADDRESS"]) + +# Load the views +from bluewind import views \ No newline at end of file diff --git a/headwind/__init__.py b/bluewind/headwind/__init__.py similarity index 66% rename from headwind/__init__.py rename to bluewind/headwind/__init__.py index a2cb260..19d955c 100644 --- a/headwind/__init__.py +++ b/bluewind/headwind/__init__.py @@ -11,6 +11,8 @@ MIN_SPEED = [0x2, 0x1] HALF_SPEED = [0x2, 0x32] FULL_SPEED = [0x2, 0x64] CHARACTERISTIC = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" +# > read the above characteristic 0xFD-01-XX-04 where XX is speed +# > bytearray(b'\xfd\x01\x00\x01') class Headwind: fanClient = None @@ -19,7 +21,23 @@ class Headwind: self.flaskApp = flaskApp self.fanClient = BleakClient(address) - async def sleep(self): + async def readSpeed(self): + try: + async with self.fanClient as client: + result = await client.read_gatt_char(CHARACTERISTIC) + return result[2] + except Exception: + return 0 + + async def readMode(self): + try: + async with self.fanClient as client: + result = await client.read_gatt_char(CHARACTERISTIC) + return result[3] # Return state code, needs to figure out what each code means + except Exception: + return 0 + + async def writeSleep(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, SLEEP) @@ -27,7 +45,7 @@ class Headwind: except Exception as e: return False - async def on(self): + async def writeOn(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, ON) @@ -35,7 +53,7 @@ class Headwind: except Exception as e: return False - async def speedMode(self): + async def writeSpeedMode(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, SPD) @@ -43,7 +61,7 @@ class Headwind: except Exception as e: return False - async def hrMode(self): + async def writeHrMode(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, HR) @@ -51,7 +69,7 @@ class Headwind: except Exception as e: return False - async def manualSpeed(self, speed): + async def writeSpeed(self, speed): if speed > 0: value = [0x2, speed] try: @@ -60,10 +78,9 @@ class Headwind: return True except Exception as e: return False - else: - return False + return False - async def off(self): + async def writeOff(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, OFF) diff --git a/headwind/spec.py b/bluewind/headwind/spec.py similarity index 70% rename from headwind/spec.py rename to bluewind/headwind/spec.py index 1b3ed14..aafc64c 100644 --- a/headwind/spec.py +++ b/bluewind/headwind/spec.py @@ -15,4 +15,16 @@ service001e = "a026ee0c-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor service001e_char001f = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor specific service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration handle = 0x0000 -address = 'F2:B3:F7:6A:24:48' \ No newline at end of file +address = 'F2:B3:F7:6A:24:48' + +# bytearray(b'\xfd\x01\x19\x04') +# 127.0.0.1 - - [10/Nov/2023 15:56:50] "GET /get/speed HTTP/1.1" 200 - +# 127.0.0.1 - - [10/Nov/2023 15:57:14] "GET /speed/32 HTTP/1.1" 200 - +# bytearray(b'\xfd\x01 \x04') +# 127.0.0.1 - - [10/Nov/2023 15:57:23] "GET /get/speed HTTP/1.1" 200 - +# 127.0.0.1 - - [10/Nov/2023 15:58:01] "GET /speed/50 HTTP/1.1" 200 - +# bytearray(b'\xfd\x012\x04') +# 127.0.0.1 - - [10/Nov/2023 15:58:14] "GET /get/speed HTTP/1.1" 200 - +# 127.0.0.1 - - [10/Nov/2023 15:58:37] "GET /speed/100 HTTP/1.1" 200 - +# bytearray(b'\xfd\x01d\x04') +# 127.0.0.1 - - [10/Nov/2023 15:58:45] "GET /get/speed HTTP/1.1" 200 - \ No newline at end of file diff --git a/bluewind/views.py b/bluewind/views.py new file mode 100755 index 0000000..bd14bc3 --- /dev/null +++ b/bluewind/views.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +from bluewind import app, fan + +@app.route("/on", methods=["POST"]) +async def setOn(): + fan_status = await fan.writeOn() + if not fan_status: + return "Failed to turn headwind on", 503 + return "Turning headwind on", 200 + +@app.route("/sleep", methods=["POST"]) +async def setSleep(): + fan_status = await fan.writeSleep() + if not fan_status: + return "Failed to put headwind to sleep", 503 + return "Putting headwind to sleep", 200 + +@app.route("/speed/", methods=["POST"]) +async def setSpeed(speed): + fan_status = await fan.writeSpeed(speed) + if not fan_status: + return f"Failed to set headwind speed to {speed}", 503 + return f"Setting headwind speed to {speed}", 200 + +@app.route("/speed/increase", methods=["POST"]) +async def setIncreaseSpeed(): + print("increase speed") + currentSpeed = await fan.readSpeed() + newSpeed = 0 + if currentSpeed < 25: + newSpeed = 25 + elif currentSpeed < 50: + newSpeed = 50 + elif currentSpeed < 75: + newSpeed = 75 + elif currentSpeed < 100: + newSpeed = 100 + fan_status = await fan.writeSpeed(newSpeed) + if not fan_status: + return f"Failed to increase speed by 25%", 503 + return f"Increased speed by 25%", 200 + +@app.route("/speed/decrease", methods=["POST"]) +async def setDecreaseSpeed(): + print("decrease speed") + currentSpeed = await fan.readSpeed() + newSpeed = 0 + if currentSpeed == 100: + newSpeed = 75 + elif currentSpeed >= 75: + newSpeed = 50 + elif currentSpeed >= 50: + newSpeed = 25 + elif currentSpeed > 25: + newSpeed = 25 + elif currentSpeed <= 25: + newSpeed = 1 + fan_status = await fan.writeSpeed(newSpeed) + if not fan_status: + return f"Failed to decrease speed by 25%", 503 + return f"Decreased speed by 25%", 200 + +@app.route("/speed", methods=["GET"]) +async def getSpeed(): + speed = await fan.readSpeed() + return f"{speed}", 200 + +@app.route("/hr", methods=["POST"]) +async def setHr(): + fan_status = await fan.writeHr() + if not fan_status: + return "Failed to set headwind to HR mode", 503 + return "Setting headwind to HR mode", 200 + +@app.route("/off", methods=["POST"]) +async def setOff(): + fan_status = await fan.off() + if not fan_status: + return "Failed to turn headwind off", 503 + return "Turning headwind off", 200 \ No newline at end of file diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index cee1e6d..0000000 --- a/config/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -class Configurinator: - address = "" - cmd = "" - speed = "" - - def __init__(self): - pass - - def load_config(self, address, cmd, speed): - self.address = address - self.cmd = cmd - self.speed = speed diff --git a/default.env b/default.env index f627499..eac4c0d 100644 --- a/default.env +++ b/default.env @@ -1 +1,2 @@ -FLASK_ADDRESS='F2:B3:F7:6A:24:48' \ No newline at end of file +FLASK_ADDRESS='F2:B3:F7:6A:24:48' +FLASK_URL='127.0.0.1:5000' \ No newline at end of file diff --git a/main.py b/main.py index a0ad445..72d2478 100755 --- a/main.py +++ b/main.py @@ -1,32 +1,5 @@ #!/usr/bin/env python3 -import asyncio -import click -from config import Configurinator as Config -from headwind import Headwind as Headwind +from bluewind import app -@click.command() -@click.option('--address', default=None, help='headwind mac address') -@click.option('--cmd', default=None, help='command to send') -@click.option('--speed', default=1, help='manual speed value, 1 to 100') -def main(address, cmd, speed): - conf = Config() - conf.load_config(address, cmd, speed) - asyncio.run(bluewind(conf)) - -async def bluewind(conf): - fan = Headwind(conf.address) - match conf.cmd: - case 'on': - print("turning fan on") - await fan.on() - case 'sleep': - print("putting fan to sleep") - await fan.sleep() - case 'manual': - print("setting fan speed") - await fan.speed(conf.speed) - print('stuff') - - -if __name__ == "__main__": - main() \ No newline at end of file +if __name__ == '__main__': + app.run(debug=False) \ No newline at end of file diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100644 index 0000000..ddece06 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Export the environment variables +if [ ! -f ".env" ]; then + echo "The .env file does not exist, please make a copy of default.env into .env and adjust its contents before proceeding." + exit 1 +fi +export $(cat .env) + +# Create new systemd file +touch /etc/systemd/system/bluewind.service + +# Fill in the systemd file +echo -e """[Unit] +Description=Flask Application +After=multi-user.target + +[Service] +Type=simple +ExecStart=$(pwd)/.venv/bin/python3 $(pwd)/main.py +Restart=always +Environment="FLASK_ADDRESS=$(echo $FLASK_ADDRESS)" +Environment="FLASK_URL=$(echo $FLASK_URL)" + +[Install] +WantedBy=multi-user.target +""" > /etc/systemd/system/bluewind.service + +# Reload systemd unit, start and enable the service +systemctl daemon-reload +systemctl enable bluewind.service +systemctl start bluewind.service \ No newline at end of file