87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
import argparse
|
|
import os
|
|
import subprocess
|
|
|
|
import httpx
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
HOST = os.environ["HOST"]
|
|
API_KEY = os.environ["API_KEY"]
|
|
USERNAME = os.environ["UNIFI_USERNAME"]
|
|
PASSWORD = os.environ["UNIFI_PASSWORD"]
|
|
|
|
BASE = f"https://{HOST}/proxy/protect/integration/v1"
|
|
PRIVATE_BASE = f"https://{HOST}/proxy/protect/api"
|
|
|
|
CAMERA_NAME = "G6 Pro 360"
|
|
DEFAULT_AUDIO_FILE = "./data/hello.wav"
|
|
DEFAULT_SPEAKER_VOLUME = 75 # 0-100, above ~85 overdrives the amp
|
|
|
|
|
|
def set_speaker_volume(camera_id: str, volume: int):
|
|
# Accept-Encoding must be suppressed at request level — server returns 403 when present
|
|
with httpx.Client(verify=False, http2=True) as client:
|
|
res = client.post(
|
|
f"https://{HOST}/api/auth/login",
|
|
content=f'{{"username":"{USERNAME}","password":"{PASSWORD}","rememberMe":false}}'.encode(),
|
|
headers={"Content-Type": "application/json", "Accept-Encoding": ""},
|
|
)
|
|
res.raise_for_status()
|
|
csrf = res.headers.get("x-csrf-token")
|
|
|
|
def get_volumes():
|
|
s = client.get(f"{PRIVATE_BASE}/cameras/{camera_id}").raise_for_status().json()["speakerSettings"]
|
|
return s["volume"], s["speakerVolume"]
|
|
|
|
before = get_volumes()
|
|
print(f"Speaker volume before: volume={before[0]} speakerVolume={before[1]}")
|
|
|
|
client.patch(
|
|
f"{PRIVATE_BASE}/cameras/{camera_id}",
|
|
headers={"x-csrf-token": csrf},
|
|
json={"speakerSettings": {"volume": volume, "speakerVolume": volume}},
|
|
).raise_for_status()
|
|
|
|
after = get_volumes()
|
|
print(f"Speaker volume after: volume={after[0]} speakerVolume={after[1]}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Play audio through the G6 Pro 360 speaker")
|
|
parser.add_argument("file", nargs="?", default=DEFAULT_AUDIO_FILE, help="Audio file to play (default: %(default)s)")
|
|
parser.add_argument("-v", "--volume", type=int, default=DEFAULT_SPEAKER_VOLUME, metavar="0-100", help="Speaker volume 0-100 (default: %(default)s)")
|
|
args = parser.parse_args()
|
|
|
|
with httpx.Client(headers={"X-API-Key": API_KEY}, verify=False) as client:
|
|
cameras = client.get(f"{BASE}/cameras").raise_for_status().json()
|
|
camera = next(c for c in cameras if CAMERA_NAME in c["name"])
|
|
print(f"Camera: {camera['name']} ({camera['id']})")
|
|
|
|
set_speaker_volume(camera["id"], args.volume)
|
|
|
|
session = client.post(f"{BASE}/cameras/{camera['id']}/talkback-session").raise_for_status().json()
|
|
print(f"Talkback: {session['url']} ({session['codec']} {session['samplingRate']}Hz)")
|
|
|
|
proc = subprocess.Popen([
|
|
"ffmpeg", "-re", "-i", args.file,
|
|
"-vn",
|
|
"-af", "volume=0.8,highpass=f=200,treble=g=-9:f=4000,equalizer=f=1000:width_type=o:width=1:g=3,alimiter=limit=0.8:level=false",
|
|
"-acodec", "libopus",
|
|
"-ar", str(session["samplingRate"]),
|
|
"-ac", "1",
|
|
"-b:a", "96k",
|
|
"-f", "rtp", session["url"],
|
|
])
|
|
|
|
try:
|
|
proc.wait()
|
|
except KeyboardInterrupt:
|
|
proc.terminate()
|
|
proc.wait()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|