Compare commits
No commits in common. "21c9b05182dd7ae88ed6a298c58e80d984731937" and "8ca102de26c23ad64896570ef8f3921c6b5901df" have entirely different histories.
21c9b05182
...
8ca102de26
|
@ -159,3 +159,6 @@ cython_debug/
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# 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.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
bluewind
|
||||||
|
bluewind/*
|
23
README.md
23
README.md
|
@ -6,29 +6,10 @@ Bluetooth control app for Wahoo Headwind.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
To run the flask app for development and testing:
|
To run the flask app:
|
||||||
```bash
|
```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='<headwind bluetooth address>'
|
export FLASK_ADDRESS='<headwind bluetooth address>'
|
||||||
export FLASK_URL='<address and port to bind'
|
flask --app api run --host=0.0.0.0
|
||||||
python3 main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the flask app in a permanent deployment:
|
|
||||||
```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
|
|
||||||
cp default.env .env # modify your .env file!
|
|
||||||
sudo /bin/bash scripts/install.sh # will install and start+enable the systemd service for you
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from flask import Flask
|
||||||
|
from headwind import Headwind as Headwind
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config.from_prefixed_env()
|
||||||
|
|
||||||
|
@app.route("/on")
|
||||||
|
async def on():
|
||||||
|
fan = Headwind(app, app.config["ADDRESS"])
|
||||||
|
fan_status = await fan.on()
|
||||||
|
if not fan_status:
|
||||||
|
return "<p>Failed to turn headwind on</p>", 503
|
||||||
|
return "<p>Turning headwind on</p>", 200
|
||||||
|
|
||||||
|
@app.route("/sleep")
|
||||||
|
async def sleep():
|
||||||
|
fan = Headwind(app, app.config["ADDRESS"])
|
||||||
|
fan_status = await fan.sleep()
|
||||||
|
if not fan_status:
|
||||||
|
return "<p>Failed to put headwind to sleep</p>", 503
|
||||||
|
return "<p>Putting headwind to sleep</p>", 200
|
||||||
|
|
||||||
|
@app.route("/speed/<int:speed>")
|
||||||
|
async def speed(speed):
|
||||||
|
fan = Headwind(app, app.config["ADDRESS"])
|
||||||
|
fan_status = await fan.manualSpeed(speed)
|
||||||
|
if not fan_status:
|
||||||
|
return f"<p>Failed to set headwind speed to {speed}</p>", 503
|
||||||
|
return f"<p>Setting headwind speed to {speed}</p>", 200
|
||||||
|
|
||||||
|
@app.route("/hr")
|
||||||
|
async def hr():
|
||||||
|
fan = Headwind(app, app.config["ADDRESS"])
|
||||||
|
fan_status = await fan.hr()
|
||||||
|
if not fan_status:
|
||||||
|
return "<p>Failed to set headwind to HR mode</p>", 503
|
||||||
|
return "<p>Setting headwind to HR mode</p>", 200
|
||||||
|
|
||||||
|
@app.route("/off")
|
||||||
|
async def off():
|
||||||
|
fan = Headwind(app, app.config["ADDRESS"])
|
||||||
|
fan_status = await fan.off()
|
||||||
|
if not fan_status:
|
||||||
|
return "<p>Failed to turn headwind off</p>", 503
|
||||||
|
return "<p>Turning headwind off</p>", 200
|
|
@ -1,12 +0,0 @@
|
||||||
# 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
|
|
|
@ -1,80 +0,0 @@
|
||||||
#!/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/<int: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
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
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
|
|
@ -1,2 +1 @@
|
||||||
FLASK_ADDRESS='F2:B3:F7:6A:24:48'
|
FLASK_ADDRESS='F2:B3:F7:6A:24:48'
|
||||||
FLASK_URL='127.0.0.1:5000'
|
|
|
@ -11,8 +11,6 @@ MIN_SPEED = [0x2, 0x1]
|
||||||
HALF_SPEED = [0x2, 0x32]
|
HALF_SPEED = [0x2, 0x32]
|
||||||
FULL_SPEED = [0x2, 0x64]
|
FULL_SPEED = [0x2, 0x64]
|
||||||
CHARACTERISTIC = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b"
|
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:
|
class Headwind:
|
||||||
fanClient = None
|
fanClient = None
|
||||||
|
@ -21,23 +19,7 @@ class Headwind:
|
||||||
self.flaskApp = flaskApp
|
self.flaskApp = flaskApp
|
||||||
self.fanClient = BleakClient(address)
|
self.fanClient = BleakClient(address)
|
||||||
|
|
||||||
async def readSpeed(self):
|
async def sleep(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:
|
try:
|
||||||
async with self.fanClient as client:
|
async with self.fanClient as client:
|
||||||
await client.write_gatt_char(CHARACTERISTIC, SLEEP)
|
await client.write_gatt_char(CHARACTERISTIC, SLEEP)
|
||||||
|
@ -45,7 +27,7 @@ class Headwind:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def writeOn(self):
|
async def on(self):
|
||||||
try:
|
try:
|
||||||
async with self.fanClient as client:
|
async with self.fanClient as client:
|
||||||
await client.write_gatt_char(CHARACTERISTIC, ON)
|
await client.write_gatt_char(CHARACTERISTIC, ON)
|
||||||
|
@ -53,7 +35,7 @@ class Headwind:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def writeSpeedMode(self):
|
async def speedMode(self):
|
||||||
try:
|
try:
|
||||||
async with self.fanClient as client:
|
async with self.fanClient as client:
|
||||||
await client.write_gatt_char(CHARACTERISTIC, SPD)
|
await client.write_gatt_char(CHARACTERISTIC, SPD)
|
||||||
|
@ -61,7 +43,7 @@ class Headwind:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def writeHrMode(self):
|
async def hrMode(self):
|
||||||
try:
|
try:
|
||||||
async with self.fanClient as client:
|
async with self.fanClient as client:
|
||||||
await client.write_gatt_char(CHARACTERISTIC, HR)
|
await client.write_gatt_char(CHARACTERISTIC, HR)
|
||||||
|
@ -69,7 +51,7 @@ class Headwind:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def writeSpeed(self, speed):
|
async def manualSpeed(self, speed):
|
||||||
if speed > 0:
|
if speed > 0:
|
||||||
value = [0x2, speed]
|
value = [0x2, speed]
|
||||||
try:
|
try:
|
||||||
|
@ -78,9 +60,10 @@ class Headwind:
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
return False
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
async def writeOff(self):
|
async def off(self):
|
||||||
try:
|
try:
|
||||||
async with self.fanClient as client:
|
async with self.fanClient as client:
|
||||||
await client.write_gatt_char(CHARACTERISTIC, OFF)
|
await client.write_gatt_char(CHARACTERISTIC, OFF)
|
|
@ -16,15 +16,3 @@ service001e_char001f = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor
|
||||||
service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration
|
service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration
|
||||||
handle = 0x0000
|
handle = 0x0000
|
||||||
address = 'F2:B3:F7:6A:24:48'
|
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 -
|
|
33
main.py
33
main.py
|
@ -1,5 +1,32 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from bluewind import app
|
import asyncio
|
||||||
|
import click
|
||||||
|
from config import Configurinator as Config
|
||||||
|
from headwind import Headwind as Headwind
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@click.command()
|
||||||
app.run(debug=False)
|
@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()
|
|
@ -1,31 +0,0 @@
|
||||||
#!/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
|
|
Loading…
Reference in New Issue