diff --git a/README.md b/README.md index d3ea499..a4d8e89 100644 --- a/README.md +++ b/README.md @@ -17,5 +17,13 @@ Usage - Run tests: `bash test.sh` - See `src/gridresilience_studio/` for core implementations and `adapters/` for starter adapters. -Note +- EnergiBridge & MVP Extensions +- This repository now includes a lightweight EnergiBridge canonical bridge to map GridResilience primitives to vendor-agnostic representations and starter adapters for IEC 61850 and a microgrid simulator. These scaffolds enable cross-domain interoperability while preserving offline-first operation. +- The MVP wiring includes: 2 starter adapters, a minimal LocalProblem/SharedSignals/PlanDelta sketch, and a toy delta-sync surface that can replay deterministically. See src/gridresilience_studio/energi_bridge.py and adapters/ for details. +- This remains a seed MVP; additional phases will introduce governance ledger, secure identities, and cross-domain dashboards in subsequent iterations. + +- Usage +- Install: `pip install -e .` +- Run tests: `bash test.sh` +- See `src/gridresilience_studio/` for core implementations and `adapters/` for starter adapters. - This repository is the MVP seed; follow-on agents will extend functionality, governance, and cross-domain testing. diff --git a/adapters/iec61850_adapter.py b/adapters/iec61850_adapter.py new file mode 100644 index 0000000..a370d8f --- /dev/null +++ b/adapters/iec61850_adapter.py @@ -0,0 +1,29 @@ +"""Starter IEC 61850 adapter scaffold for GridResilience Studio.""" +from __future__ import annotations + +from typing import Any, Dict + + +class IEC61850Adapter: + def __init__(self, host: str = "127.0.0.1", port: int = 553, use_tls: bool = True) -> None: + self.host = host + self.port = port + self.use_tls = use_tls + self.connected = False + + def connect(self) -> bool: + # Lightweight scaffold: pretend to connect + self.connected = True + return True + + def send_command(self, target_id: str, command: Dict[str, Any]) -> Dict[str, Any]: + if not self.connected: + raise RuntimeError("Not connected to IEC61850 endpoint") + # Placeholder: echo back the command for testing purposes + return {"status": "ok", "target": target_id, "command": command} + + def read_signals(self) -> Dict[str, Any]: + if not self.connected: + raise RuntimeError("Not connected to IEC61850 endpoint") + # Placeholder signals + return {"voltage": 1.0, "frequency": 50.0} diff --git a/adapters/simulator_adapter.py b/adapters/simulator_adapter.py new file mode 100644 index 0000000..ecbfdc6 --- /dev/null +++ b/adapters/simulator_adapter.py @@ -0,0 +1,28 @@ +"""Starter microgrid simulator adapter scaffold for GridResilience Studio.""" +from __future__ import annotations + +from typing import Any, Dict + + +class SimulatorAdapter: + def __init__(self, name: str = "sim-bridge") -> None: + self.name = name + self.running = False + + def start(self) -> bool: + self.running = True + return True + + def stop(self) -> None: + self.running = False + + def get_signals(self) -> Dict[str, Any]: + if not self.running: + return {} + # Return a tiny synthetic signal set for demonstration + return {"sim_voltage": 230.0, "sim_freq": 50.0} + + def apply_command(self, obj_id: str, command: Dict[str, Any]) -> Dict[str, Any]: + if not self.running: + return {"status": "stopped"} + return {"status": "applied", "object": obj_id, "command": command} diff --git a/src/gridresilience_studio/__init__.py b/src/gridresilience_studio/__init__.py index afe75d0..8c28d01 100644 --- a/src/gridresilience_studio/__init__.py +++ b/src/gridresilience_studio/__init__.py @@ -4,9 +4,17 @@ Public API surface: - Objects: LocalDevicePlans (DERs, loads, pumps) - Morphisms: SharedSignals (versioned telemetry and policy signals) - PlanDelta: incremental islanding/load-shedding updates with cryptographic tags -- core helpers: delta-sync, governance scaffold +- EnergiBridge: canonical bridge to map GridResilience primitives to vendor-agnostic representations """ from .core import Object, Morphism, PlanDelta __all__ = ["Object", "Morphism", "PlanDelta"] + +# Optional EnergiBridge export (kept optional to avoid hard import on all usages) +try: + from .energi_bridge import EnergiBridge # type: ignore + __all__.append("EnergiBridge") +except Exception: + # EnergiBridge is not required for core unit tests; skip if unavailable + pass diff --git a/src/gridresilience_studio/energi_bridge.py b/src/gridresilience_studio/energi_bridge.py new file mode 100644 index 0000000..909fd35 --- /dev/null +++ b/src/gridresilience_studio/energi_bridge.py @@ -0,0 +1,53 @@ +"""EnergiBridge: Canonical bridge for GridResilience primitives. + +This module provides lightweight mapping helpers that translate between +GridResilience Studio canonical primitives (Objects, Morphisms, PlanDelta) +and a vendor-agnostic representation suitable for cross-domain adapters. +The implementation here is intentionally small and dependency-free to keep +tests fast and focused on integration surface. +""" +from __future__ import annotations +from dataclasses import dataclass, field +from typing import Any, Dict + +from .core import Object, Morphism, PlanDelta + + +@dataclass +class LocalProblem: + """Toy DSL: Local problem description in a cross-domain friendly form.""" + id: str + description: str + resources: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SharedSignals: + """Toy representation of signals shared between devices.""" + id: str + source: str + target: str + signals: Dict[str, Any] = field(default_factory=dict) + version: int = 0 + + +class EnergiBridge: + """Static helpers to map between GridResilience primitives and canonical forms.""" + + @staticmethod + def map_object(lp: LocalProblem) -> Object: + """Map a LocalProblem into an Object representation.""" + return Object(id=lp.id, type="LocalProblem", properties={"description": lp.description, **lp.resources}) + + @staticmethod + def map_signals(sig: SharedSignals) -> Morphism: + """Map SharedSignals into a Morphism representation.""" + return Morphism(id=sig.id, source=sig.source, target=sig.target, signals=sig.signals, version=sig.version) + + @staticmethod + def map_delta(delta: PlanDelta) -> PlanDelta: + """Identity mapping for PlanDelta (pass-through in this sketch).""" + return delta + + +__all__ = ["LocalProblem", "SharedSignals", "EnergiBridge"]