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'

HomeCore

Controller connected

Controller: {s["controller_id"]}

Property: {s.get("property_id")}

This Green is permanently bound to the HomeCore property.

' return STYLE+'''

HomeCore

Set up this controller

Sign in with the customer's HomeCore Owner account. HomeCore will create or select the property and permanently bind this Home Assistant Green.

''' @app.post('/bind',response_class=HTMLResponse) def bind(email:str=Form(...),password:str=Form(...),property_name:str=Form(...)): if load_state().get('controller_id'):raise HTTPException(409,'Controller already bound') api=options()['cloud_api'].rstrip('/') with httpx.Client(timeout=20) as c: r=c.post(api+'/api/v1/auth/login',json={'email':email,'password':password});r.raise_for_status();token=r.json()['access_token'];headers={'Authorization':'Bearer '+token} r=c.get(api+'/api/v1/properties',headers=headers);r.raise_for_status();found=next((p for p in r.json() if p['name'].lower()==property_name.lower() and p.get('owner')),None) if not found: r=c.post(api+'/api/v1/properties',headers=headers,json={'name':property_name,'timezone':'Europe/London'});r.raise_for_status();found=r.json() info=supervisor_get('/core/info') or {};ha_ver=((info.get('data') or {}).get('version') or '') if isinstance(info,dict) else '' payload={'property_id':found['id'],'public_key':pub_pem(),'fingerprint':fingerprint(),'software_version':'0.1.0','ha_version':ha_ver} r=c.post(api+'/api/v1/controllers/bind',headers=headers,json=payload);r.raise_for_status();out=r.json() save_state(out) return HTMLResponse(STYLE+'

HomeCore

Connected

This controller is now bound to the customer property. Sign into the HomeCore app with the same Owner account.

') @app.get('/health') def health():return {'status':'ok','bound':bool(load_state().get('controller_id')),'fingerprint':fingerprint()} threading.Thread(target=heartbeat_loop,daemon=True).start() if __name__=='__main__':uvicorn.run(app,host='0.0.0.0',port=8099)