diff --git a/homecore_controller/CHANGELOG.md b/homecore_controller/CHANGELOG.md index c4ef831..326e8cf 100644 --- a/homecore_controller/CHANGELOG.md +++ b/homecore_controller/CHANGELOG.md @@ -1,5 +1,18 @@ # HomeCore Controller Changelog +## 1.0.3 + +- Replaces the old scan-first device setup with the expandable HomeCore Add Device catalogue. +- Adds HomeCore-led configuration flows for Shelly, Philips Hue, Yale Smart Living, Yale Home and Hive when their controller drivers are available. +- Adds catalogue entries and driver detection for Hikvision AX PRO, Heatmiser Neo/legacy, the newer Yale Smart Alarm range and Sky Glass so these can use HomeCore-compatible drivers without redesigning the UI. +- Adds setup variants for Hue Bridge/Bluetooth and Shelly Wi-Fi/Ethernet, Bluetooth and Z-Wave paths. +- HomeCore now starts and advances Home Assistant configuration flows itself; customers no longer need to open Home Assistant Devices & Services for supported products. +- Adds generic HomeCore form rendering for credentials, addresses, verification codes, menus, external sign-in and setup progress. +- After a product is connected, HomeCore immediately offers its discovered devices for naming, room assignment and favourites. +- Expands HomeCore entity discovery to include alarm/security binary sensors and general sensor entities. +- Preserves the separate HomeCore Updater and manual per-controller Admin deployment architecture. +- Automatic rollout remains disabled. + ## 1.0.2 - Reworks HomeCore setup and Settings from Waterbeck field testing. diff --git a/homecore_controller/app/main.py b/homecore_controller/app/main.py index 9aaeea6..eb4a006 100644 --- a/homecore_controller/app/main.py +++ b/homecore_controller/app/main.py @@ -17,7 +17,7 @@ from cryptography.hazmat.primitives import serialization import uvicorn from updater import start_update_manager -VERSION = "1.0.2" +VERSION = "1.0.3" print( "HOMECORE SUPERVISOR: token " @@ -184,7 +184,7 @@ DEVICE_DOMAINS = { "light", "climate", "water_heater", "camera", "cover", "alarm_control_panel", "media_player", "switch", "lock", "event", "binary_sensor", "fan", "scene", "script", - "automation", "button", "remote", "vacuum", + "automation", "button", "remote", "vacuum", "sensor", "number", "select", } CATEGORY_LABELS = { @@ -200,13 +200,164 @@ CATEGORY_LABELS = { "fan": "Fans", "scene": "Scenes", "automation": "Automations", + "sensor": "Sensors", "other": "Other", } + +HOMECORE_DEVICE_CATALOG = [ + { + "id": "shelly", + "name": "Shelly", + "category": "Smart devices", + "description": "Relays, switches, dimmers, plugs, shutters, meters and supported Shelly devices.", + "capabilities": ["Lighting", "Switching", "Blinds", "Energy"], + "variants": [ + { + "id": "shelly", + "name": "Shelly Wi-Fi / Ethernet", + "handlers": ["shelly"], + "note": "HomeCore discovers the Shelly on your network or lets you enter its address.", + }, + { + "id": "shelly_bluetooth", + "name": "Shelly BLU / Bluetooth", + "handlers": ["bthome"], + "note": "For compatible Shelly BLU devices exposed through BTHome.", + }, + { + "id": "shelly_zwave", + "name": "Shelly Wave / Z-Wave", + "handlers": ["zwave_js"], + "note": "Requires a Z-Wave radio/controller connected to the HomeCore Green.", + }, + ], + }, + { + "id": "philips_hue", + "name": "Philips Hue", + "category": "Lighting", + "description": "Hue lights, lamps, light strips, switches, remotes and motion sensors.", + "capabilities": ["Lighting", "Scenes", "Sensors"], + "variants": [ + { + "id": "hue_bridge", + "name": "Hue Bridge", + "handlers": ["hue"], + "note": "Recommended. HomeCore discovers the Hue Bridge and guides you through pairing.", + }, + { + "id": "hue_bluetooth", + "name": "Hue Bluetooth", + "handlers": ["hue_ble"], + "note": "For supported Hue Bluetooth lights when a compatible Bluetooth adapter is available.", + }, + ], + }, + { + "id": "hikvision_axpro", + "name": "Hikvision AX PRO", + "category": "Alarm & security", + "description": "AX PRO / AX Hub alarm panel, zones and supported wireless detectors.", + "capabilities": ["Alarm", "Zones", "Sensors"], + "variants": [ + { + "id": "hikvision_axpro", + "name": "Hikvision AX PRO", + "handlers": ["hikvision_axpro"], + "note": "Uses the HomeCore-compatible AX PRO local driver when installed.", + "unavailable": "The AX PRO driver is not installed on this Green yet.", + } + ], + }, + { + "id": "yale", + "name": "Yale", + "category": "Alarm & security", + "description": "Yale Smart Living/Sync alarms plus supported Yale Home devices.", + "capabilities": ["Alarm", "Locks", "Sensors", "Doorbell"], + "variants": [ + { + "id": "yale_smart_living", + "name": "Yale Smart Living / Sync Alarm", + "handlers": ["yale_smart_alarm"], + "note": "For the established Yale Smart Living / Sync alarm platform.", + }, + { + "id": "yale_home", + "name": "Yale Home", + "handlers": ["yale"], + "note": "For Yale Home locks, doorbells and supported Yale Home devices.", + }, + { + "id": "yale_new_alarm", + "name": "New Yale Smart Alarm", + "handlers": ["yale_smart_alarm_ce"], + "note": "Uses the HomeCore-compatible driver for the newer Yale Home alarm range when installed.", + "unavailable": "The new Yale Smart Alarm driver is not installed on this Green yet.", + }, + ], + }, + { + "id": "heatmiser", + "name": "Heatmiser", + "category": "Heating", + "description": "Heatmiser thermostats, heating zones and supported NeoHub systems.", + "capabilities": ["Heating", "Thermostats", "Zones"], + "variants": [ + { + "id": "heatmiser_neo", + "name": "Heatmiser Neo / NeoHub", + "handlers": ["heatmiserneo"], + "note": "Uses the HomeCore-compatible NeoHub driver when installed.", + "unavailable": "The Heatmiser Neo driver is not installed on this Green yet.", + }, + { + "id": "heatmiser_legacy", + "name": "Heatmiser legacy gateway", + "handlers": ["heatmiser"], + "note": "For legacy Heatmiser gateways when a setup handler is available.", + "unavailable": "This Heatmiser system requires the HomeCore Heatmiser driver.", + }, + ], + }, + { + "id": "sky_glass", + "name": "Sky Glass", + "category": "Entertainment", + "description": "Sky Glass television and compatible Sky entertainment controls.", + "capabilities": ["TV", "Media"], + "variants": [ + { + "id": "sky_glass", + "name": "Sky Glass", + "handlers": ["sky_glass", "sky_stream"], + "note": "HomeCore will use a compatible Sky Glass/Stream driver when one is installed.", + "unavailable": "A compatible Sky Glass driver is not installed on this Green yet.", + } + ], + }, + { + "id": "hive", + "name": "Hive", + "category": "Heating & smart devices", + "description": "Hive heating, hot water, radiator valves, plugs, lights and supported sensors.", + "capabilities": ["Heating", "Hot water", "Lighting", "Plugs", "Sensors"], + "variants": [ + { + "id": "hive", + "name": "Hive", + "handlers": ["hive"], + "note": "Sign in with the Hive account owner. Hive requires two-factor authentication.", + } + ], + }, +] + SESSIONS = {} LOCK = threading.RLock() -APP_HTML = '\n\n\n\n\nHomeCore\n\n\n\n
Loading home…
\n
\n
HomeCore

Settings

\n
\n
◉
HomeCore Intercom
Someone is at the door
\n\n\n' +APP_HTML = '\n\n\n\n\nHomeCore\n\n\n\n
Loading home…
\n
\n
HomeCore

Settings

\n
\n
◉
HomeCore Intercom
Someone is at the door
\n\n\n' def build_router(version, data_dir, load_state, options, ha_version): @@ -445,6 +596,18 @@ def build_router(version, data_dir, load_state, options, ha_version): ).lower() if device_class == "doorbell" or "doorbell" in label: return "doorbell" + if device_class in ( + "door", "window", "opening", "motion", "occupancy", + "tamper", "lock", "problem", "safety", "smoke", "gas", + ): + return "security" + return "sensor" + if domain == "sensor": + if device_class == "temperature": + return "heating" + return "sensor" + if domain in ("number", "select"): + return "sensor" if domain == "button" and "doorbell" in entity_id.lower(): return "doorbell" if domain == "remote": @@ -511,8 +674,10 @@ def build_router(version, data_dir, load_state, options, ha_version): attrs = raw.get("attributes") or {} category = category_for(entity_id, attrs) - # Ignore binary/event noise unless it is a real doorbell/intercom signal. - if category == "other" and domain in ("binary_sensor", "event"): + # Event entities are only customer-facing when they represent + # doorbell/intercom activity. Binary sensors are retained because + # alarm ecosystems expose zones and detectors through them. + if category == "other" and domain == "event": return None override = (product.get("devices") or {}).get(entity_id, {}) @@ -887,6 +1052,209 @@ def build_router(version, data_dir, load_state, options, ha_version): "devices": discovered, } + def flow_handlers(): + handlers = ha_request( + "GET", + "/config/config_entries/flow_handlers", + timeout=30, + ) or [] + return set(str(x) for x in handlers) + + def public_catalog(): + handlers = flow_handlers() + result = [] + for brand in HOMECORE_DEVICE_CATALOG: + item = { + key: value + for key, value in brand.items() + if key != "variants" + } + variants = [] + for variant in brand.get("variants", []): + chosen = next( + ( + handler + for handler in variant.get("handlers", []) + if handler in handlers + ), + None, + ) + variants.append( + { + "id": variant["id"], + "name": variant["name"], + "note": variant.get("note", ""), + "available": bool(chosen), + "handler": chosen, + "unavailable": variant.get( + "unavailable", + "This HomeCore device driver is not available on this Green.", + ), + } + ) + item["variants"] = variants + item["available"] = any(x["available"] for x in variants) + result.append(item) + return result + + def find_catalog_variant(brand_id, variant_id): + for brand in HOMECORE_DEVICE_CATALOG: + if brand["id"] != brand_id: + continue + variants = brand.get("variants", []) + variant = next( + (x for x in variants if x["id"] == variant_id), + variants[0] if len(variants) == 1 else None, + ) + if not variant: + raise HTTPException(400, "Choose a HomeCore device type") + return brand, variant + raise HTTPException(404, "HomeCore device brand not found") + + @router.get("/api/v1/device-catalog") + def device_catalog(request: Request): + ctx = auth_context(request) + require_owner(ctx) + try: + catalogue = public_catalog() + except Exception as exc: + raise HTTPException( + 502, + "HomeCore could not read the device-driver catalogue from the controller: " + + str(exc), + ) + return { + "catalog": catalogue, + "expandable": True, + } + + @router.post("/api/v1/device-setup/start") + async def device_setup_start(request: Request): + ctx = auth_context(request) + require_owner(ctx) + body = await request.json() + brand_id = str(body.get("brand_id") or "") + variant_id = str(body.get("variant_id") or "") + brand, variant = find_catalog_variant(brand_id, variant_id) + + handlers = flow_handlers() + handler = next( + ( + name + for name in variant.get("handlers", []) + if name in handlers + ), + None, + ) + + if not handler: + raise HTTPException( + 409, + variant.get( + "unavailable", + "This HomeCore device driver is not available on this Green.", + ), + ) + + try: + step = ha_request( + "POST", + "/config/config_entries/flow", + payload={ + "handler": handler, + "show_advanced_options": False, + }, + timeout=60, + ) + except httpx.HTTPStatusError as exc: + detail = "" + try: + detail = exc.response.json().get("message", "") + except Exception: + detail = exc.response.text + raise HTTPException( + exc.response.status_code, + detail or f"Could not start {brand['name']} setup", + ) + + activity( + "device.setup.started", + { + "brand": brand["name"], + "variant": variant["name"], + "handler": handler, + }, + ) + return { + "brand": brand["name"], + "variant": variant["name"], + "handler": handler, + "step": step, + } + + @router.get("/api/v1/device-setup/flow/{flow_id}") + def device_setup_get(flow_id: str, request: Request): + ctx = auth_context(request) + require_owner(ctx) + try: + return ha_request( + "GET", + f"/config/config_entries/flow/{flow_id}", + timeout=60, + ) + except httpx.HTTPStatusError as exc: + raise HTTPException( + exc.response.status_code, + "HomeCore device setup session is no longer available", + ) + + @router.post("/api/v1/device-setup/flow/{flow_id}") + async def device_setup_continue(flow_id: str, request: Request): + ctx = auth_context(request) + require_owner(ctx) + body = await request.json() + try: + step = ha_request( + "POST", + f"/config/config_entries/flow/{flow_id}", + payload=body, + timeout=120, + ) + except httpx.HTTPStatusError as exc: + detail = "" + try: + detail = exc.response.json().get("message", "") + except Exception: + detail = exc.response.text + raise HTTPException( + exc.response.status_code, + detail or "HomeCore could not continue device setup", + ) + + if isinstance(step, dict) and step.get("type") == "create_entry": + activity( + "device.setup.completed", + { + "handler": step.get("handler", ""), + "title": step.get("title", ""), + }, + ) + return step + + @router.delete("/api/v1/device-setup/flow/{flow_id}") + def device_setup_cancel(flow_id: str, request: Request): + ctx = auth_context(request) + require_owner(ctx) + try: + ha_request( + "DELETE", + f"/config/config_entries/flow/{flow_id}", + timeout=30, + ) + except Exception: + pass + return {"ok": True} + @router.get("/api/v1/devices/{entity_id:path}") def device(entity_id: str, request: Request): ctx = auth_context(request) diff --git a/homecore_controller/config.yaml b/homecore_controller/config.yaml index c407e54..9165d49 100644 --- a/homecore_controller/config.yaml +++ b/homecore_controller/config.yaml @@ -1,5 +1,5 @@ name: HomeCore Controller -version: "1.0.2" +version: "1.0.3" slug: homecore_controller description: HomeCore whole-home control system for Home Assistant Green. url: https://homecore.home-core.co.uk