from pathlib import Path import os,json,base64,hashlib,time,threading import httpx from fastapi import FastAPI,Form,HTTPException from fastapi.responses import HTMLResponse from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from cryptography.hazmat.primitives import serialization import uvicorn DATA=Path('/config');DATA.mkdir(parents=True,exist_ok=True) STATE=DATA/'state.json';KEY=DATA/'controller_ed25519.pem';OPTIONS=Path('/data/options.json') def options(): try:return json.loads(OPTIONS.read_text()) except Exception:return {'cloud_api':'https://api.homecore.example.com'} def ensure_key(): if KEY.exists():return serialization.load_pem_private_key(KEY.read_bytes(),password=None) k=Ed25519PrivateKey.generate();KEY.write_bytes(k.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()));return k KEYOBJ=ensure_key() def pub_pem():return KEYOBJ.public_key().public_bytes(serialization.Encoding.PEM,serialization.PublicFormat.SubjectPublicKeyInfo).decode() def fingerprint():return hashlib.sha256(KEYOBJ.public_key().public_bytes(serialization.Encoding.Raw,serialization.PublicFormat.Raw)).hexdigest() def load_state(): try:return json.loads(STATE.read_text()) except Exception:return {} def save_state(s):STATE.write_text(json.dumps(s,indent=2)) def supervisor_get(path): token=os.getenv('SUPERVISOR_TOKEN') if not token:return None try: r=httpx.get('http://supervisor'+path,headers={'Authorization':'Bearer '+token},timeout=5);r.raise_for_status();return r.json() except Exception:return None def controller_session(): s=load_state() if not s.get('controller_id'):return None api=options()['cloud_api'].rstrip('/') with httpx.Client(timeout=15) as c: ch=c.get(f"{api}/api/v1/controllers/{s['controller_id']}/challenge");ch.raise_for_status();challenge=ch.json()['challenge'] sig=base64.b64encode(KEYOBJ.sign(challenge.encode())).decode() r=c.post(f"{api}/api/v1/controllers/{s['controller_id']}/session",json={'challenge':challenge,'signature_b64':sig});r.raise_for_status();return r.json()['controller_token'] def heartbeat_loop(): while True: try: s=load_state() if s.get('controller_id'): token=controller_session(); api=options()['cloud_api'].rstrip('/') info=supervisor_get('/core/info') or {}; ha_ver=((info.get('data') or {}).get('version') or '') if isinstance(info,dict) else '' httpx.post(f"{api}/api/v1/controllers/{s['controller_id']}/heartbeat",headers={'Authorization':'Bearer '+token},json={'software_version':'0.1.0','ha_version':ha_ver},timeout=10) except Exception:pass time.sleep(60) app=FastAPI(title='HomeCore Controller',version='0.1.0') STYLE='''''' @app.get('/',response_class=HTMLResponse) def setup_page(): s=load_state() if s.get('controller_id'): return STYLE+f'
Controller: {s["controller_id"]}
Property: {s.get("property_id")}
This Green is permanently bound to the HomeCore property.
Sign in with the customer's HomeCore Owner account. HomeCore will create or select the property and permanently bind this Home Assistant Green.
This controller is now bound to the customer property. Sign into the HomeCore app with the same Owner account.