diff --git a/README.md b/README.md index e45dcaf..fee2abe 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,33 @@ -# OpenEnergySphere EnergiBridge (EnergiBridge MVP) +OpenEnergySphere EnergiBridge MVP -This repository implements a production-ready skeleton for OpenEnergySphere's federated, contract-driven cross-domain energy planning platform. The MVP focuses on the EnergiBridge interoperability layer, mapping OpenEnergySphere primitives to a canonical IR and enabling plug-and-play adapters across DERs, meters, pumps, and buildings while preserving offline-first operation and governance. +Overview +- EnergiBridge provides a canonical, vendor-agnostic interoperability layer for cross-domain energy planning. It translates domain primitives into a compact IR (Object, SharedVariables, PlanDelta) and enables plug-and-play adapters across DERs, meters, pumps, and buildings while preserving offline-first operation and governance hooks. -Note: Added a minimal EnergiBridge mapper (EnergiBridgeMapper) to illustrate canonical mappings between LocalProblem objects and a vendor-agnostic IR. This enables straightforward extension to more complex cross-domain workflows and adapters. +Architecture +- Core primitives: LocalProblem (per-site optimization), SharedVariables (signals and priors), PlanDelta (incremental actions with provenance), and a Graph-of-Contracts (GoC) registry for adapters and data schemas. +- EnergiBridgeMapper (in src/energysphere/energi_bridge.py) maps Energysphere primitives to a canonical IR representation and supports round-tripping LocalProblem objects. +- Registry: In-memory GraphContractRegistry with metadata for auditing and replay protection. LocalProblems get stored with version, timestamp, and nonce metadata. +- Adapters: Starter adapters implemented under energysphere/adapters/ (SubstationMeterAdapter, DERAggregatorAdapter). +- Server API: Lightweight FastAPI app in energysphere/server/main.py exposing contract and adapter endpoints. -Architecture highlights -- Core primitives: LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog -- Graph-of-Contracts registry for adapters and data schemas with per-message metadata -- Adapters: starter implementations (substation_meter, der_aggregator) -- EnergiBridge: mapping functions between OpenEnergySphere primitives and a CatOpt-like IR -- Server: FastAPI app exposing endpoints to register contracts and adapters -- Storage: in-memory by default, with potential for SQLite/PostgreSQL persistence +Roadmap (MVP, 8–12 weeks) +- Phase 0: Protocol skeleton, 2 starter adapters, TLS transport, ADMM-lite local solver, delta-sync with deterministic replay. +- Phase 1: Governance ledger scaffold, identity management (DIDs/certs), secure aggregation for SharedVariables. +- Phase 2: Cross-domain demo with toy cross-domain objective and EnergiBridge SDK. +- Phase 3: Hardware-in-the-loop validation and KPI dashboards. -Getting started -- Install dependencies and run tests via test.sh -- Run API locally: uvicorn energysphere.server.main:app --reload +Data Contracts Seeds +- LocalProblem, SharedVariables, DualVariables, PlanDelta, PrivacyBudget, AuditLog; a Graph-of-Contracts registry; minimal contract example and conformance harness for adapters. -This README intentionally provides a compact architectural overview and a concrete MVP workflow to accelerate collaboration and iteration. +Security & Governance +- Hardware-backed attestation for adapters; DID/short-lived certs; attestation for plan deltas; optional local DP budgets. + +How to run (current MVP) +- Install dependencies and run tests: + - bash test.sh +- Start API (dev): + - uvicorn energysphere.server.main:app --reload --port 8000 + +Notes +- This project uses a pyproject.toml with a proper build-system. Packaging metadata is wired to README.md via readme = "README.md" in pyproject.toml. +- The goal is to keep a tight MVP surface while enabling gradual expansion into a production-grade, privacy-preserving federation platform. diff --git a/src/energysphere/energi_bridge.py b/src/energysphere/energi_bridge.py index d20490f..64d49cc 100644 --- a/src/energysphere/energi_bridge.py +++ b/src/energysphere/energi_bridge.py @@ -51,5 +51,26 @@ class EnergiBridgeMapper: parameters=obj.get("parameters", {}), ) + # --- Additional helpers for richer IR coverage --- + def map_shared_variable(self, sv: "SharedVariable") -> Dict[str, Any]: # type: ignore[name-defined] + """Map a SharedVariable (or similar) to a canonical IR representation.""" + return { + "type": "SharedVariable", + "name": getattr(sv, "name", ""), + "value": getattr(sv, "value", None), + "version": getattr(sv, "version", 1), + "privacy_bound": getattr(sv, "privacy_bound", None), + } + + def map_plan_delta(self, delta: "PlanDelta") -> Dict[str, Any]: # type: ignore[name-defined] + """Map a PlanDelta to a canonical IR representation.""" + return { + "type": "PlanDelta", + "delta_id": getattr(delta, "delta_id", ""), + "changes": getattr(delta, "changes", {}), + "timestamp": getattr(delta, "timestamp", 0.0), + "author": getattr(delta, "delta_id", ""), + } + __all__ = ["EnergiBridgeMapper"] diff --git a/src/energysphere/registry.py b/src/energysphere/registry.py index 9edaee2..c66f827 100644 --- a/src/energysphere/registry.py +++ b/src/energysphere/registry.py @@ -1,5 +1,7 @@ from __future__ import annotations +import time +import secrets from typing import Any, Dict, List from .models import LocalProblem @@ -16,6 +18,36 @@ class GraphContractRegistry: self._contracts: Dict[str, Dict[str, Any]] = {} def register_contract(self, contract_id: str, contract_spec: Dict[str, Any]) -> None: + """Register a contract specification with optional metadata. + + This is a very light-weight in-memory store. Contracts can carry + arbitrary metadata under a dedicated _meta key, but we preserve the + contract_spec as-is for backward-compatibility. + """ + self._contracts[contract_id] = contract_spec + + def register_contract_with_meta( + self, + contract_id: str, + contract_spec: Dict[str, Any], + version: int | None = None, + timestamp: float | None = None, + nonce: str | None = None, + ) -> None: + """Register a contract with explicit metadata for auditing/replay protection.""" + if version is None: + version = 1 + if timestamp is None: + timestamp = time.time() + if nonce is None: + nonce = secrets.token_hex(8) + meta = { + "version": int(version), + "timestamp": float(timestamp), + "nonce": nonce, + } + contract_spec = dict(contract_spec) # shallow copy + contract_spec["_meta"] = meta self._contracts[contract_id] = contract_spec def list_contracts(self) -> List[Dict[str, Any]]: @@ -26,4 +58,11 @@ class GraphContractRegistry: # Lightweight helper to store a LocalProblem as a contract reference def register_local_problem(self, contract_id: str, lp: LocalProblem) -> None: - self.register_contract(contract_id, lp.dict()) + """Register a LocalProblem with default metadata.""" + spec = lp.dict() + spec["_meta"] = { + "version": 1, + "timestamp": time.time(), + "nonce": secrets.token_hex(8), + } + self.register_contract(contract_id, spec)