Skill: mrbese/grid-aware-energy-load-shifter (version 1.0.3)

--- name: grid-aware-energy-load-shifter description: > Grid-aware energy load shifter for Home Assistant. Reads real-time electricity prices (TOU, time-of-use, dynamic pricing), solar production forecasts, battery state of charge, and consumption data from Home Assistant. Schedules deferrable household loads (EV charging, HVAC pre-conditioning, pool pump, dishwasher, laundry, water heater) to cheapest rate windows. Calculates cost savings, optimizes solar self-consumption, and supports virtual power plant (VPP) demand response signals. Works with Nordpool, ENTSO-e, Tibber, Octopus Energy, Amber Electric, and any utility rate plan worldwide. Built for distributed energy resource (DER) optimization and residential demand-side management. metadata: openclaw: emoji: "⚡" requires: env: - HA_URL - HA_TOKEN tags: - energy - home-assistant - load-shifting - time-of-use - solar - battery-storage - demand-response - vpp - ev-charging - smart-home - DER - grid-optimization --- # Grid-Aware Energy Load Shifter Shift heavy residential loads to the cheapest electricity hours using Home Assistant energy data. ## Quick Start ```bash # Find all energy-related entities in HA python3 {baseDir}/scripts/ha_bridge.py discover # Get a full energy dashboard snapshot (prices, solar, consumption, batteries) python3 {baseDir}/scripts/ha_bridge.py energy-summary # Turn on the EV charger python3 {baseDir}/scripts/ha_bridge.py call-service switch/turn_on --entity-id switch.ev_charger ``` ## Connection Two paths to reach Home Assistant: 1. **MCP (preferred):** If the HA MCP server is configured, use `mcporter call homeassistant.<tool>` directly. 2. **REST API:** Use `python3 {baseDir}/scripts/ha_bridge.py`. Requires `HA_URL` and `HA_TOKEN` environment variables. ## Security **Required credentials:** | Variable | Description | |---|---| | `HA_URL` | Home Assistant base URL (e.g. `http://homeassistant.local:8123`) | | `HA_TOKEN` | Home Assistant Long-Lived Access Token | **Least-privilege recommendations:** - Create a dedicated Home Assistant user account for this skill (e.g. `openclaw-energy`) - Generate a Long-Lived Access Token from that account only - Limit the account's entity access to energy-related entities if your HA setup supports entity-level permissions - Test with read-only commands first (`discover`, `energy-summary`) before enabling device control **Domain allowlist:** The `call-service` command restricts actions to energy-related domains only: `switch`, `automation`, `script`, `climate`, `water_heater`, `input_boolean`, `input_number`, `number`. All other domains (e.g. `lock`, `alarm_control_panel`) are blocked with exit code 2. ## Commands | Command | What it does | Example | |---|---|---| | `discover` | List all energy entities | `ha_bridge.py discover` | | `energy-summary` | One-shot dashboard (prices + consumption + solar + storage) | `ha_bridge.py energy-summary` | | `status <entity>` | Read a single entity's state and attributes | `ha_bridge.py status sensor.electricity_price` | | `call-service <d/s>` | Call an energy-related HA service (restricted to allowed domains) | `ha_bridge.py call-service switch/turn_on --entity-id switch.ev_charger` | | `history <entity>` | Get state changes over last N hours | `ha_bridge.py history sensor.grid_import --hours 24` | All commands output JSON to stdout. ## Load-Shifting Workflow Follow these steps when asked about energy optimization: 1. **Discover** available energy entities: run `discover` or `energy-summary` 2. **Read prices**: Check pricing entities' state and attributes — look for: - Hourly price arrays in `today` / `tomorrow` / `prices_today` / `rates` attributes - `price_level` attribute (CHEAP / NORMAL / EXPENSIVE) - Current vs. average price comparison 3. **Identify deferrable loads**: Find `switch.*` entities for schedulable devices (EV charger, pool pump, dishwasher, washer/dryer, water heater) 4. **Find the cheapest window**: Scan hourly prices for the contiguous N-hour block with the lowest sum (N = estimated run time of device) 5. **Execute**: Call `switch/turn_on` at the optimal time, or `automation/trigger` if the user has an existing automation ## Interpreting Price Data Different integrations expose prices differently: - **Hourly arrays** (Nordpool, ENTSO-e, Octopus): Read `today`/`tomorrow` attributes → find cheapest hours - **Price level** (Tibber): Read `price_level` → act when CHEAP or VERY_CHEAP - **Real-time** (Amber Electric): Read 5-minute pricing → shift loads immediately when cheap - **Utility meter tariffs**: Read `sensor.*_peak` vs `sensor.*_offpeak` → user's HA automations switch tariffs at configured times - **Static TOU**: Read `current_price` attribute → compare against historical average ## Cost Savings Estimate When recommending a shift, show estimated savings: ``` savings = (current_rate - cheapest_rate) × device_power_kw × run_duration_hours ``` ## Solar Self-Consumption If solar sensors exist, align loads with peak production: - Read `sensor.forecast_solar_*` or `sensor.solcast_*` for today's forecast - Shift loads to hours with highest expected production - This avoids grid import entirely — savings = full retail rate × kWh shifted ## HVAC Pre-Conditioning HVAC is the largest residential load (40-50% of electricity). Pre-cool or pre-heat during cheap/solar hours so the home coasts through expensive peak periods: 1. Read `climate.*` entities for current HVAC mode and setpoint 2. During cheapest window: lower cooling setpoint by 2-3F (pre-cool) or raise heating setpoint by 2-3F (pre-heat) 3. During peak window: raise cooling setpoint by 2-3F to coast on thermal mass 4. Savings estimate: 1.5-3 kW shifted × price differential × hours ## Water Heater Scheduling Electric water heaters (4.5 kW typical) are ideal deferrable loads: 1. Find `switch.water_heater` or `water_heater.*` entities 2. Heat during cheapest/solar window to full temperature 3. Turn off during peak hours (tank maintains temperature for 4-6 hours) 4. Savings estimate: 4.5 kW × price differential × 3-4 hours/day ## Battery Arbitrage If home battery entities exist (`sensor.battery_soc`, `sensor.powerwall_*`, `sensor.enphase_*`): 1. Read current state of charge and charge/discharge rate limits 2. Charge from grid during cheapest hours (or from solar) 3. Discharge to home during peak price hours to avoid grid import 4. Advanced: If battery supports grid export and VPP enrollment, discharge to grid during extreme price events ($2,000+/MWh) 5. Savings estimate: battery_capacity_kwh × (peak_rate - valley_rate) ## Demand Response / VPP Integration For homes enrolled in utility demand response or virtual power plant programs: 1. Read demand response signal entities (if available via HA integration) 2. When DR event active: shed non-critical loads, pre-cool/pre-heat, discharge battery 3. Estimate DR payment: kW reduced × event duration × program rate ## Entity Reference For detailed entity patterns across providers, read: [energy_entities.md](references/energy_entities.md)

path: README.md
content: # Grid-Aware Energy Load Shifter

An OpenClaw skill that bridges Home Assistant's energy management data to an autonomous AI agent. Reads real-time electricity prices, solar production forecasts, battery state of charge, and consumption data from Home Assistant. Schedules deferrable household loads - EV charging, HVAC pre-conditioning, pool pumps, water heaters, dishwashers, and laundry - to the cheapest rate windows, optimizing solar self-consumption and supporting demand response / virtual power plant (VPP) signals.

## What This Skill Does

| Capability | Description |
|---|---|
| **Price-Aware Scheduling** | Reads TOU (time-of-use), dynamic, and real-time pricing from Nordpool, ENTSO-e, Tibber, Octopus Energy, Amber Electric, and any utility rate plan worldwide |
| **Solar Self-Consumption** | Aligns load timing with solar production forecasts (Solcast, Forecast.Solar) to minimize grid import |
| **Battery Arbitrage** | Charges home batteries (Tesla Powerwall, Enphase) during off-peak/solar, discharges during peak |
| **HVAC Pre-Conditioning** | Pre-cools or pre-heats during cheap hours so the home coasts through expensive peak periods |
| **Water Heater Scheduling** | Heats during cheapest window, coasts through peak (4.5 kW typical load) |
| **Demand Response / VPP** | Sheds load during DR events, estimates DR payments for enrolled homes |
| **Cost Estimation** | Calculates per-device savings from each recommended load shift |

## Files

| File | Purpose |
|---|---|
| `SKILL.md` | Agent-facing instructions with load-shifting workflow, price interpretation, and DER optimization strategies |
| `scripts/ha_bridge.py` | REST API bridge - 5 CLI commands, zero pip dependencies, Python stdlib only |
| `references/energy_entities.md` | Comprehensive entity reference for 7+ energy pricing providers and all HA energy entity patterns |

## Quick Start

```bash
# Set HA connection (REST API mode)
export HA_URL=http://homeassistant.local:8123
export HA_TOKEN=your_long_lived_access_token

# Discover energy entities
python3 scripts/ha_bridge.py discover

# Get full energy dashboard
python3 scripts/ha_bridge.py energy-summary

# Control a device
python3 scripts/ha_bridge.py call-service switch/turn_on --entity-id switch.ev_charger
```

## Technical Design

- **Zero external dependencies** - uses only Python standard library (`urllib`, `json`, `argparse`)
- **Universal compatibility** - no hardcoded rate schedules; reads whatever pricing entities HA provides
- **Dual connectivity** - works via HA MCP server or REST API fallback
- **JSON output** - all commands emit structured JSON for agent parsing
- **Exit codes** - `0` success, `1` API error, `2` configuration error

## Keywords

Distributed energy resources (DER), demand-side management (DSM), time-of-use (TOU) rate optimization, dynamic electricity pricing, residential load shifting, grid-edge intelligence, virtual power plant (VPP), demand response (DR), behind-the-meter optimization, home energy management system (HEMS), smart grid, energy arbitrage, solar self-consumption, battery storage optimization, EV managed charging, HVAC pre-conditioning, thermal storage.

---

Built by [Omer Bese](https://linkedin.com/in/omerbese) | Energy Systems Engineer | Columbia University MS Sustainability Management


path: scripts/ha_bridge.py
content: #!/usr/bin/env python3
"""
ha_bridge.py - Home Assistant REST API bridge for OpenClaw.

A lightweight CLI that lets the OpenClaw agent read energy data and call
services on a local Home Assistant instance. Zero external dependencies;
uses only Python stdlib.

Usage:
    python3 ha_bridge.py <command> [options]

Commands:
    status          Get the state of a specific entity
    discover        Find all energy-related entities
    energy-summary  One-shot energy dashboard snapshot
    call-service    Call a Home Assistant service
    history         Fetch recent state history for an entity

Environment:
    HA_URL    Base URL of Home Assistant (e.g. http://homeassistant.local:8123)
    HA_TOKEN  Long-Lived Access Token for authentication

Exit codes:
    0  Success
    1  API / network error
    2  Configuration error (missing HA_URL or HA_TOKEN)
"""

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone


# ---------------------------------------------------------------------------
#  Configuration
# ---------------------------------------------------------------------------


def load_config() -> dict:
    """
    Read HA_URL and HA_TOKEN from the environment.

    Returns a config dict or exits with code 2 if not set.
    """
    ha_url = os.environ.get("HA_URL", "").rstrip("/")
    ha_token = os.environ.get("HA_TOKEN", "")

    errors = []
    if not ha_url:
        errors.append("HA_URL is not set")
    if not ha_token:
        errors.append("HA_TOKEN is not set")

    if errors:
        _error(
            "Configuration error: " + "; ".join(errors) + ". "
            "Set these environment variables.",
            exit_code=2,
        )

    return {"url": ha_url, "token": ha_token}


# ---------------------------------------------------------------------------
#  HA REST API Client
# ---------------------------------------------------------------------------


def ha_request(method: str, endpoint: str, config: dict, payload: dict = None) -> dict:
    """
    Make an authenticated HTTP request to the Home Assistant REST API.

    Args:
        method:   HTTP method (GET or POST)
        endpoint: API path, e.g. "/api/states/sensor.electricity_price"
        config:   Dict with 'url' and 'token' keys
        payload:  Optional JSON body for POST requests

    Returns:
        Parsed JSON response (dict or list).

    Raises SystemExit on network / API errors (exit code 1).
    """
    url = f"{config['url']}{endpoint}"
    headers = {
        "Authorization": f"Bearer {config['token']}",
        "Content-Type": "application/json",
    }

    data = json.dumps(payload).encode("utf-8") if payload else None
    req = urllib.request.Request(url, data=data, headers=headers, method=method)

    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            body = resp.read().decode("utf-8")
            return json.loads(body) if body else {}
    except urllib.error.HTTPError as exc:
        detail = ""
        try:
            detail = exc.read().decode("utf-8")
        except Exception:
            pass
        _error(f"HA API returned HTTP {exc.code} for {method} {endpoint}: {detail}")
    except urllib.error.URLError as exc:
        _error(f"Cannot reach Home Assistant at {config['url']}: {exc.reason}")
    except Exception as exc:
        _error(f"Unexpected error talking to HA: {exc}")


# ---------------------------------------------------------------------------
#  Read Commands
# ---------------------------------------------------------------------------


# Energy-related device_class values used for auto-discovery.
ENERGY_DEVICE_CLASSES = {
    "energy",       # kWh sensors (grid import, export, device consumption)
    "power",        # W/kW instantaneous power draw
    "monetary",     # Cost / price sensors
    "battery",      # Battery state-of-charge
    "gas",          # Gas consumption
    "voltage",      # Grid voltage
    "current",      # Amperage
}


def get_entity_state(entity_id: str, config: dict) -> dict:
    """
    Fetch the full state object for a single entity.

    Returns dict with: entity_id, state, attributes, last_changed, last_updated.
    """
    result = ha_request("GET", f"/api/states/{entity_id}", config)
    return _format_state(result)


def discover_energy_entities(config: dict) -> list:
    """
    Scan all HA entities and return those relevant to energy management.

    Filters by device_class (energy, power, monetary, battery, gas) and also
    catches entities whose entity_id contains energy-related keywords.
    """
    all_states = ha_request("GET", "/api/states", config)

    # Keywords to match in entity_id for entities that may lack device_class
    ENERGY_KEYWORDS = {
        "energy", "power", "electricity", "solar", "battery", "grid",
        "consumption", "production", "tariff", "price", "cost", "kwh",
        "charger", "ev_charger", "inverter", "soc",
    }

    results = []
    for entity in all_states:
        attrs = entity.get("attributes", {})
        device_class = attrs.get("device_class", "")
        eid = entity.get("entity_id", "")

        # Match by device_class
        if device_class in ENERGY_DEVICE_CLASSES:
            results.append(_format_state(entity))
            continue

        # Match by keyword in entity_id
        eid_lower = eid.lower()
        if any(kw in eid_lower for kw in ENERGY_KEYWORDS):
            results.append(_format_state(entity))

    return results


def build_energy_summary(config: dict) -> dict:
    """
    Build a one-shot energy dashboard snapshot.

    Groups discovered energy entities into categories:
      - pricing:     Electricity price / cost sensors
      - consumption: Grid import, device energy usage
      - production:  Solar / wind generation
      - storage:     Battery SOC and power
      - devices:     Controllable switches and high-draw appliances

    This gives the agent everything it needs to reason about load shifting
    in a single call.
    """
    entities = discover_energy_entities(config)

    summary = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "pricing": [],
        "consumption": [],
        "production": [],
        "storage": [],
        "devices": [],
        "other_energy": [],
    }

    for ent in entities:
        eid = ent["entity_id"].lower()
        dc = ent.get("attributes", {}).get("device_class", "")

        if dc == "monetary" or _matches_any(eid, ["price", "cost", "tariff", "rate"]):
            summary["pricing"].append(ent)
        elif dc == "battery" or _matches_any(eid, ["battery", "soc", "storage"]):
            summary["storage"].append(ent)
        elif _matches_any(eid, ["solar", "pv", "production", "generation", "inverter"]):
            summary["production"].append(ent)
        elif _matches_any(eid, ["grid", "consumption", "import", "usage", "demand"]):
            summary["consumption"].append(ent)
        elif eid.startswith("switch.") or _matches_any(eid, ["charger", "ev", "dishwasher", "washer", "dryer", "pool", "pump", "heater"]):
            summary["devices"].append(ent)
        else:
            summary["other_energy"].append(ent)

    # Add counts for quick scanning
    summary["entity_counts"] = {
        cat: len(items) for cat, items in summary.items()
        if isinstance(items, list)
    }

    return summary


def get_entity_history(entity_id: str, hours: float, config: dict) -> list:
    """
    Fetch the state history for an entity over the last N hours.

    Uses GET /api/history/period/<timestamp>?filter_entity_id=<id>
    Returns a flat list of state change objects.
    """
    start = datetime.now(timezone.utc) - timedelta(hours=hours)
    ts = start.strftime("%Y-%m-%dT%H:%M:%S")
    endpoint = f"/api/history/period/{ts}?filter_entity_id={entity_id}&minimal_response&no_attributes"
    result = ha_request("GET", endpoint, config)

    # API returns [[state1, state2, ...]] - a list of lists grouped by entity
    if result and isinstance(result, list) and len(result) > 0:
        return result[0]
    return []


# ---------------------------------------------------------------------------
#  Write Commands
# ---------------------------------------------------------------------------


# Service domains allowed for call-service. Restricted to energy-related
# domains to prevent misuse (e.g. lock/unlock, alarm_control_panel).
ALLOWED_SERVICE_DOMAINS = {
    "switch",          # Toggle devices (EV charger, pool pump, water heater)
    "automation",      # Trigger energy automations
    "script",          # Run energy management scripts
    "climate",         # HVAC pre-conditioning setpoints
    "water_heater",    # Water heater scheduling
    "input_boolean",   # Toggle helper entities
    "input_number",    # Set numeric helpers (e.g. setpoint overrides)
    "number",          # Device number controls (e.g. charge current limit)
}


def call_service(domain: str, service: str, data: dict, config: dict) -> dict:
    """
    Call a Home Assistant service (energy-related domains only).

    Args:
        domain:  Service domain (e.g. "switch", "automation", "script")
        service: Service name (e.g. "turn_on", "trigger")
        data:    Service data dict (typically includes "entity_id")
        config:  HA connection config

    Example:
        call_service("switch", "turn_on", {"entity_id": "switch.ev_charger"}, cfg)

    Returns the API response (list of affected states).

    Raises SystemExit (code 2) if the domain is not in the allowlist.
    """
    if domain not in ALLOWED_SERVICE_DOMAINS:
        _error(
            f"Domain '{domain}' is not allowed. "
            f"Permitted domains: {', '.join(sorted(ALLOWED_SERVICE_DOMAINS))}. "
            "This skill is restricted to energy-related services.",
            exit_code=2,
        )
    endpoint = f"/api/services/{domain}/{service}"
    return ha_request("POST", endpoint, config, payload=data)


# ---------------------------------------------------------------------------
#  Helpers
# ---------------------------------------------------------------------------


def _format_state(raw: dict) -> dict:
    """Normalize an HA state object to a clean, flat-ish dict."""
    return {
        "entity_id": raw.get("entity_id", ""),
        "state": raw.get("state", ""),
        "attributes": raw.get("attributes", {}),
        "last_changed": raw.get("last_changed", ""),
        "last_updated": raw.get("last_updated", ""),
    }


def _matches_any(text: str, keywords: list) -> bool:
    """Return True if any keyword appears in text."""
    return any(kw in text for kw in keywords)


def _output(data) -> None:
    """Print JSON to stdout (agent-facing output)."""
    print(json.dumps(data, indent=2, default=str))


def _error(message: str, exit_code: int = 1) -> None:
    """Print error to stderr and exit."""
    print(json.dumps({"error": message}), file=sys.stderr)
    sys.exit(exit_code)


# ---------------------------------------------------------------------------
#  CLI Argument Parsing
# ---------------------------------------------------------------------------


def build_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser with all subcommands."""
    parser = argparse.ArgumentParser(
        prog="ha_bridge.py",
        description=(
            "Home Assistant energy bridge for OpenClaw. "
            "Read energy data and call services via the HA REST API."
        ),
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    # --- status ---
    p_status = subparsers.add_parser(
        "status",
        help="Get the state of a specific entity",
    )
    p_status.add_argument(
        "entity_id",
        help="Entity ID to query (e.g. sensor.electricity_price)",
    )

    # --- discover ---
    subparsers.add_parser(
        "discover",
        help="Find all energy-related entities in Home Assistant",
    )

    # --- energy-summary ---
    subparsers.add_parser(
        "energy-summary",
        help="One-shot energy dashboard snapshot (prices, consumption, solar, batteries)",
    )

    # --- call-service ---
    p_call = subparsers.add_parser(
        "call-service",
        help="Call a Home Assistant service (e.g. switch/turn_on)",
    )
    p_call.add_argument(
        "service_target",
        help="Service as domain/service (e.g. switch/turn_on, automation/trigger)",
    )
    p_call.add_argument(
        "--entity-id",
        required=True,
        help="Target entity ID (e.g. switch.ev_charger)",
    )
    p_call.add_argument(
        "--data",
        default="{}",
        help='Additional service data as JSON string (e.g. \'{"brightness": 100}\')',
    )

    # --- history ---
    p_hist = subparsers.add_parser(
        "history",
        help="Fetch recent state history for an entity",
    )
    p_hist.add_argument(
        "entity_id",
        help="Entity ID to query history for",
    )
    p_hist.add_argument(
        "--hours",
        type=float,
        default=24,
        help="How many hours of history to fetch (default: 24)",
    )

    return parser


# ---------------------------------------------------------------------------
#  Main
# ---------------------------------------------------------------------------


def main():
    """CLI entry point."""
    parser = build_parser()
    args = parser.parse_args()
    config = load_config()

    if args.command == "status":
        result = get_entity_state(args.entity_id, config)
        _output(result)

    elif args.command == "discover":
        results = discover_energy_entities(config)
        _output({"count": len(results), "entities": results})

    elif args.command == "energy-summary":
        summary = build_energy_summary(config)
        _output(summary)

    elif args.command == "call-service":
        # Parse "domain/service" format
        parts = args.service_target.split("/", 1)
        if len(parts) != 2:
            _error(
                f"Invalid service format '{args.service_target}'. "
                "Use domain/service (e.g. switch/turn_on)",
                exit_code=2,
            )
        domain, service = parts

        # Build service data
        try:
            extra_data = json.loads(args.data)
        except json.JSONDecodeError as exc:
            _error(f"Invalid JSON in --data: {exc}", exit_code=2)

        service_data = {"entity_id": args.entity_id, **extra_data}
        result = call_service(domain, service, service_data, config)
        _output({"service": f"{domain}.{service}", "result": result})

    elif args.command == "history":
        results = get_entity_history(args.entity_id, args.hours, config)
        _output({"entity_id": args.entity_id, "hours": args.hours, "states": results})


if __name__ == "__main__":
    main()


path: references/energy_entities.md
content: # Home Assistant Energy Entity Reference

Reference for interpreting energy-related entities in Home Assistant. Read this when you need deeper context about entity attributes or discovery patterns.

## Table of Contents

1. [Electricity Price Sensors](#electricity-price-sensors)
2. [Consumption Sensors](#consumption-sensors)
3. [Solar Production Sensors](#solar-production-sensors)
4. [Battery / Storage Sensors](#battery--storage-sensors)
5. [Utility Meter Entities](#utility-meter-entities)
6. [Controllable Devices](#controllable-devices)
7. [HVAC & Water Heater Entities](#hvac--water-heater-entities)
8. [Common HA Services for Load Control](#common-ha-services-for-load-control)
9. [Supported Integrations](#supported-integrations)

---

## Electricity Price Sensors

These track the current electricity rate from the user's utility provider.

| Integration | Typical Entity ID | Key Attributes |
|---|---|---|
| Nordpool | `sensor.nordpool_kwh_*` | `today` (24-element array of hourly prices), `tomorrow` (available after ~13:00), `current_price`, `average`, `peak`, `off_peak_1`, `off_peak_2`, `unit` |
| Tibber | `sensor.tibber_prices` | `price_level` (VERY_CHEAP / CHEAP / NORMAL / EXPENSIVE / VERY_EXPENSIVE), `estimated_annual_consumption` |
| ENTSO-e | `sensor.entsoe_*` | `prices_today` (hourly array), `prices_tomorrow`, `min_price`, `max_price`, `avg_price` |
| Amber Electric | `sensor.amber_*` | `price`, `renewables_percentage`, `spike_status` |
| Octopus Energy | `sensor.octopus_energy_*` | `rates` (array of rate objects with `start`, `end`, `value_inc_vat`) |
| Manual / Static | `sensor.electricity_price` | `current_price`, `unit_of_measurement` (e.g. USD/kWh) |
| Utility Meter Tariff | `sensor.daily_electricity_peak` | `tariff` (peak / offpeak / shoulder), `status` |

**How to find the cheapest window:** Read the hourly price array attributes (`today`/`tomorrow`/`prices_today`/`rates`), find the contiguous block of N hours with the lowest sum. That's your optimal load-shifting window.

---

## Consumption Sensors

Track energy drawn from the grid or consumed by devices.

| Pattern | Example Entity | Description |
|---|---|---|
| Grid import | `sensor.grid_consumption`, `sensor.grid_import_energy` | Total kWh imported from grid |
| Grid export | `sensor.grid_export_energy`, `sensor.grid_feed_in` | kWh returned to grid (solar) |
| Device power | `sensor.<device>_power` | Instantaneous watts (W) |
| Device energy | `sensor.<device>_energy` | Cumulative kWh consumed |
| Whole-home | `sensor.house_consumption`, `sensor.total_power` | Sum of all loads |

**Required attributes for Energy Dashboard compatibility:**
- `device_class: energy`
- `state_class: total_increasing`
- `unit_of_measurement: kWh`

---

## Solar Production Sensors

| Pattern | Example Entity | Key Attributes |
|---|---|---|
| Current production | `sensor.solar_production`, `sensor.pv_power` | Instantaneous W or kW |
| Total production | `sensor.solar_energy_today` | Cumulative kWh today |
| Forecast | `sensor.energy_production_today`, `sensor.energy_production_tomorrow` | From Forecast.Solar or Solcast |
| Solcast detail | `sensor.solcast_pv_forecast_*` | `detailedForecast` (30-min intervals), `forecast_today`, `forecast_tomorrow` |

**Self-consumption optimization:** Compare solar production forecast against planned load timing. Shifting loads to align with solar peaks avoids grid import entirely.

---

## Battery / Storage Sensors

| Pattern | Example Entity | Key Attributes |
|---|---|---|
| State of charge | `sensor.<battery>_soc`, `sensor.<battery>_battery_level` | Value in %, `device_class: battery` |
| Power flow | `sensor.<battery>_power` | Positive = charging, negative = discharging (W) |
| Capacity | `sensor.<battery>_energy_capacity` | Total capacity in kWh |

**Battery arbitrage:** Charge during off-peak or solar surplus. Discharge during peak to avoid expensive grid import.

---

## Utility Meter Entities

Home Assistant's Utility Meter helper creates tariff-aware sensors.

| Example | Description |
|---|---|
| `sensor.daily_electricity_peak` | kWh consumed during peak tariff |
| `sensor.daily_electricity_offpeak` | kWh consumed during off-peak tariff |
| `sensor.monthly_electricity` | Monthly total |

**Tariff switching:** HA automations call `utility_meter.select_tariff` at scheduled times to switch between `peak` and `offpeak`.

---

## Controllable Devices

These are the entities you control to shift loads:

| Domain | Example | Action |
|---|---|---|
| `switch` | `switch.ev_charger`, `switch.pool_pump`, `switch.dishwasher` | `switch.turn_on` / `switch.turn_off` |
| `automation` | `automation.charge_ev_offpeak` | `automation.trigger` to run now |
| `script` | `script.start_laundry_cycle` | `script.turn_on` to execute |
| `input_boolean` | `input_boolean.allow_ev_charging` | Toggle to enable/disable automations |
| `climate` | `climate.water_heater` | `climate.set_temperature` to pre-heat |

---

## HVAC & Water Heater Entities

| Pattern | Example Entity | Key Attributes |
|---|---|---|
| HVAC | `climate.thermostat`, `climate.heat_pump` | `hvac_mode`, `temperature`, `current_temperature` |
| Water heater | `switch.water_heater`, `water_heater.tank` | `current_temperature`, `target_temp_high` |
| HVAC power | `sensor.hvac_power`, `sensor.heat_pump_energy` | `device_class: power`, unit `W` or `kW` |

---

## Common HA Services for Load Control

```
switch.turn_on       {"entity_id": "switch.ev_charger"}
switch.turn_off      {"entity_id": "switch.pool_pump"}
automation.trigger    {"entity_id": "automation.offpeak_dishwasher"}
script.turn_on       {"entity_id": "script.delayed_charge"}
climate.set_temperature  {"entity_id": "climate.thermostat", "temperature": 68}
homeassistant.turn_on  {"entity_id": "switch.dryer"}   # generic, works on any domain
homeassistant.turn_off {"entity_id": "switch.dryer"}
```

---

## Supported Integrations

| Integration | Price Entity Pattern | Forecast Available |
|---|---|---|
| Nordpool | `sensor.nordpool_*` | today + tomorrow arrays |
| ENTSO-e | `sensor.entsoe_*` | today + tomorrow arrays |
| Tibber | `sensor.tibber_*` | price_level attribute |
| Octopus Energy | `sensor.octopus_*` | current + upcoming rates |
| Amber Electric | `sensor.amber_*` | real-time 5-min pricing |
| Solcast | `sensor.solcast_*` | solar forecast |
| Forecast.Solar | `sensor.forecast_solar_*` | solar forecast |
| Tesla Powerwall | `sensor.powerwall_*` | battery SOC + grid status |
| Enphase | `sensor.enphase_*` | battery + solar + grid |
| Pila Energy | (upcoming) | mesh battery fleet |


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)