diff --git a/homecore_controller/CHANGELOG.md b/homecore_controller/CHANGELOG.md
index f652a20..c4ef831 100644
--- a/homecore_controller/CHANGELOG.md
+++ b/homecore_controller/CHANGELOG.md
@@ -1,5 +1,22 @@
# HomeCore Controller Changelog
+## 1.0.2
+
+- Reworks HomeCore setup and Settings from Waterbeck field testing.
+- Adds a real Add Devices flow: HomeCore now scans supported Home Assistant entities and lets the customer explicitly add selected devices.
+- Splits Devices into My Devices and Add Devices, with HomeCore name, room assignment, favourites, rename and remove controls.
+- Removing a device from HomeCore leaves the underlying Home Assistant device/configuration untouched.
+- Removes room icon selection; room creation and editing now use the room name only.
+- Reorganises Settings into Home, Rooms, Devices, People & access, Activity and Controller.
+- Adds a first-run setup panel guiding the customer to create rooms and add devices.
+- Replaces the Local indicator with Cloud connected / Syncing / Offline states.
+- Removes duplicate Overview navigation when there are no rooms; one-room navigation is simplified.
+- Improves empty Overview cards and setup/activity empty states.
+- Removes duplicate home-name presentation from Overview; the saved HomeCore home name remains in the header.
+- Adds explicit Home Assistant device-scan diagnostics so an empty device list is no longer silent.
+- Preserves the separate HomeCore Updater and manual per-controller Admin deployment architecture.
+- Automatic rollout remains disabled.
+
## 1.0.1
- Repackages the complete HomeCore 1.0 product as a repository-safe update from 0.1.18.
diff --git a/homecore_controller/app/main.py b/homecore_controller/app/main.py
index 13f11c6..9aaeea6 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.1"
+VERSION = "1.0.2"
print(
"HOMECORE SUPERVISOR: token "
@@ -206,7 +206,7 @@ CATEGORY_LABELS = {
SESSIONS = {}
LOCK = threading.RLock()
-APP_HTML = '\n\n
\n\n\nHomeCore\n\n\n\n
HomeCore
Loading home…
\n\n
HomeCore
Settings
\n
\n
◉
HomeCore Intercom
Someone is at the door
\n'
+APP_HTML = '\n\n\n\n\nHomeCore\n\n\n\n
HomeCore
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):
@@ -499,21 +499,33 @@ def build_router(version, data_dir, load_state, options, ha_version):
return ["view"]
return []
- def normalize(raw, product):
+ def normalize(raw, product, include_unadded=False):
entity_id = str(raw.get("entity_id") or "")
if "." not in entity_id:
return None
+
domain = entity_id.split(".", 1)[0]
if domain not in DEVICE_DOMAINS:
return None
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"):
return None
override = (product.get("devices") or {}).get(entity_id, {})
- if override.get("hidden"):
+
+ # A device becomes part of HomeCore only when the customer explicitly
+ # adds it. Legacy configured entries from 1.0.1 count as added.
+ added = bool(override.get("added"))
+ if override and "added" not in override:
+ added = True
+
+ if not include_unadded and not added:
+ return None
+ if not include_unadded and override.get("hidden"):
return None
name = (
@@ -532,6 +544,7 @@ def build_router(version, data_dir, load_state, options, ha_version):
"state": raw.get("state"),
"room_id": override.get("room_id") or "",
"favorite": bool(override.get("favorite")),
+ "added": added,
"available": raw.get("state") not in ("unavailable", "unknown"),
"last_changed": raw.get("last_changed"),
"last_updated": raw.get("last_updated"),
@@ -558,17 +571,23 @@ def build_router(version, data_dir, load_state, options, ha_version):
)
return item
+ def all_homeassistant_states():
+ states = ha_request("GET", "/states") or []
+ if not isinstance(states, list):
+ raise RuntimeError("Home Assistant did not return an entity-state list")
+ return states
+
def get_devices(ctx=None):
product = load_product()
- states = ha_request("GET", "/states") or []
out = []
- for raw in states:
- item = normalize(raw, product)
+ for raw in all_homeassistant_states():
+ item = normalize(raw, product, include_unadded=False)
if not item:
continue
if ctx and not can_view(ctx, item["category"]):
continue
out.append(item)
+
out.sort(
key=lambda x: (
x["room_id"],
@@ -578,6 +597,27 @@ def build_router(version, data_dir, load_state, options, ha_version):
)
return out
+ def get_discovered_devices(ctx=None):
+ product = load_product()
+ states = all_homeassistant_states()
+ out = []
+ for raw in states:
+ item = normalize(raw, product, include_unadded=True)
+ if not item:
+ continue
+ if ctx and not can_view(ctx, item["category"]):
+ continue
+ out.append(item)
+
+ out.sort(
+ key=lambda x: (
+ 0 if x.get("added") else 1,
+ x["category"],
+ x["name"].lower(),
+ )
+ )
+ return states, out
+
def overview(devices):
lighting = [x for x in devices if x["category"] == "lighting"]
heating = [x for x in devices if x["category"] == "heating"]
@@ -835,6 +875,18 @@ def build_router(version, data_dir, load_state, options, ha_version):
ctx = auth_context(request)
return {"devices": get_devices(ctx)}
+ @router.get("/api/v1/device-discovery")
+ def device_discovery(request: Request):
+ ctx = auth_context(request)
+ require_owner(ctx)
+ states, discovered = get_discovered_devices(ctx)
+ return {
+ "total_states": len(states),
+ "supported_count": len(discovered),
+ "added_count": sum(1 for x in discovered if x.get("added")),
+ "devices": discovered,
+ }
+
@router.get("/api/v1/devices/{entity_id:path}")
def device(entity_id: str, request: Request):
ctx = auth_context(request)
@@ -1039,7 +1091,6 @@ def build_router(version, data_dir, load_state, options, ha_version):
room = {
"id": "room_" + uuid.uuid4().hex[:12],
"name": name,
- "icon": str(body.get("icon") or "⌂")[:8],
"order": len(product["rooms"]),
}
product["rooms"].append(room)
@@ -1075,10 +1126,6 @@ def build_router(version, data_dir, load_state, options, ha_version):
"Room name is required",
)
room["name"] = name[:60]
- if "icon" in body:
- room["icon"] = str(
- body.get("icon") or "⌂"
- )[:8]
if "order" in body:
room["order"] = int(body["order"])
@@ -1149,6 +1196,10 @@ def build_router(version, data_dir, load_state, options, ha_version):
value["favorite"] = bool(
body["favorite"]
)
+ if "added" in body:
+ value["added"] = bool(
+ body["added"]
+ )
if "hidden" in body:
value["hidden"] = bool(
body["hidden"]
@@ -1166,6 +1217,19 @@ def build_router(version, data_dir, load_state, options, ha_version):
"settings": value,
}
+ @router.get("/api/v1/cloud/status")
+ def cloud_status(request: Request):
+ auth_context(request)
+ api = options()["cloud_api"].rstrip("/")
+ try:
+ response = httpx.get(api + "/health", timeout=5)
+ return {
+ "connected": response.status_code < 500,
+ "status": "connected" if response.status_code < 500 else "offline",
+ }
+ except Exception:
+ return {"connected": False, "status": "offline"}
+
@router.get("/api/v1/cloud/access")
def cloud_access(request: Request):
ctx = auth_context(request)
diff --git a/homecore_controller/config.yaml b/homecore_controller/config.yaml
index 5fd6b51..c407e54 100644
--- a/homecore_controller/config.yaml
+++ b/homecore_controller/config.yaml
@@ -1,5 +1,5 @@
name: HomeCore Controller
-version: "1.0.1"
+version: "1.0.2"
slug: homecore_controller
description: HomeCore whole-home control system for Home Assistant Green.
url: https://homecore.home-core.co.uk