Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions skills/battery-lifecycle-observer/skill.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ sensors_required:
protocol: growatt
- type: victron_grid_power
protocol: victron
- type: battery
protocol: simulation

triggers:
- name: battery_deep_discharge_risk
Expand All @@ -32,6 +34,14 @@ triggers:
escalate_to: local_slm
action_tier: A

# Tier D — Critical SOC emergency cutoff: prevent deep discharge damage
# Covers simulation (sensor_type='battery'), Growatt, Victron, and hook-derived is_soc_sensor.
- name: battery_emergency_cutoff
condition: "(sensor_type == 'battery' or sensor_type == 'growatt_battery_soc' or sensor_type == 'victron_battery_soc' or is_soc_sensor == 1) and value < battery_critical_threshold"
bypass_llm: true
action_tier: D
cooldown_seconds: 0

- name: olax_voltage_decay_degraded
condition: "is_voltage_mode == 1 and is_battery_voltage_sensor == 1 and outage_active == 1 and outage_duration_minutes >= olax_min_outage_minutes and voltage_decay_v_per_hour >= olax_decay_threshold_v_per_hour"
cooldown_seconds: 1800
Expand All @@ -53,6 +63,13 @@ prompts:
Do not include timestamps, percentages, units, currency, or action statements.
Example: "Your battery has been cycling heavily this week, which can speed up wear."

battery_emergency_cutoff: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
the battery was cut off before full discharge.
Do not include timestamps, percentages, units, currency, or action statements.
Example: "The backup battery dropped critically low, so Ori cut it off to prevent permanent damage."

olax_voltage_decay_degraded: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
Expand All @@ -66,10 +83,13 @@ actions:
tier: A
- name: log_to_dashboard
tier: A
- name: switch_to_grid
tier: D
defaults:
battery_deep_discharge_risk: [alert_whatsapp, log_to_dashboard]
battery_cycle_stress: [alert_whatsapp, log_to_dashboard]
olax_voltage_decay_degraded: [alert_whatsapp, log_to_dashboard]
battery_emergency_cutoff: [switch_to_grid, alert_whatsapp, log_to_dashboard]

config:
timezone: Africa/Lagos
Expand All @@ -80,6 +100,9 @@ config:
low_soc_persistence_minutes: 30
efc_warning_threshold_weekly: 7.0

# Emergency cutoff threshold (Tier D)
battery_critical_threshold: 5.0

# Voltage-proxy mode (PoC OLAX UPS path)
battery_voltage_sensor_ids: ["olax-battery-voltage"]
grid_voltage_sensor_ids: ["grid-voltage"]
Expand Down
47 changes: 46 additions & 1 deletion skills/energy-anomaly-detector/skill.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ sensors_required:
protocol: i2c
- type: current
protocol: serial
- type: voltage
protocol: serial

triggers:
- name: sustained_overdraw
Expand All @@ -32,11 +34,27 @@ triggers:
action_tier: A

- name: dangerous_overcurrent
condition: "value > dangerous_overcurrent_threshold"
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and value > dangerous_overcurrent_threshold"
bypass_llm: true
action_tier: D
cooldown_seconds: 0

# Tier D — Grid overvoltage: protect equipment from damaging voltage levels
- name: voltage_overvoltage
condition: "sensor_type == 'voltage' and sensor_id == 'grid-main_voltage' and value > overvoltage_threshold"
bypass_llm: true
action_tier: D
cooldown_seconds: 0

# Tier B — PHCN grid restored: switch load back from generator to grid
# phcn_restore_active is injected by hooks when load is confirmed on generator.
# Default is 0 (safe for deployments without live context).
- name: grid_phcn_restored
condition: "sensor_type == 'voltage' and sensor_id == 'grid-main_voltage' and value >= grid_restore_voltage and phcn_restore_active == 1"
action_tier: B
reasoning_policy: post_action
cooldown_seconds: 0

prompts:
sustained_overdraw: |
Your job is diagnosis only.
Expand Down Expand Up @@ -74,12 +92,32 @@ prompts:
Example: "A dangerous power surge was detected and this can damage
equipment quickly."

voltage_overvoltage: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
a sudden voltage spike is dangerous to connected equipment.
Do not include timestamps, currency, percentages, units, or action statements.
Example: "An unusually high voltage arrived from the grid and can damage sensitive electronics."

grid_phcn_restored: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
Ori switched the building back to grid power.
Do not include timestamps, currency, percentages, units, or action statements.
Example: "PHCN power returned to normal levels, so Ori switched the building back to grid supply."

actions:
available:
- name: alert_whatsapp
tier: A
- name: log_to_dashboard
tier: A
# Tier D relay actions: override in deployment config after wiring is verified.
# See dangerous_overcurrent default comment below.
- name: switch_to_generator
tier: D
- name: switch_to_grid
tier: B
defaults:
sustained_overdraw: [alert_whatsapp, log_to_dashboard]
sudden_load_spike: [alert_whatsapp, log_to_dashboard]
Expand All @@ -92,6 +130,8 @@ actions:
# after relay wiring is physically verified and mapped in `actions.relay`.
# Example override: [trip_relay, alert_whatsapp, log_to_dashboard]
dangerous_overcurrent: [alert_whatsapp, log_to_dashboard]
voltage_overvoltage: [switch_to_generator, alert_whatsapp, log_to_dashboard]
grid_phcn_restored: [switch_to_grid, alert_whatsapp, log_to_dashboard]

config:
min_quality: 0.8
Expand All @@ -110,3 +150,8 @@ config:
# line_voltage: 230.0
power_factor: 0.9
dangerous_overcurrent_threshold: 20.0
# Grid voltage triggers
overvoltage_threshold: 260.0
grid_restore_voltage: 200.0
# Injected to 1 by hooks when load is confirmed on generator; 0 = safe default
phcn_restore_active: 0
28 changes: 28 additions & 0 deletions skills/hvac-refrigerant-monitor/skill.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ sensors_required:
protocol: i2c
- type: current_clamp
protocol: i2c
- type: current
protocol: serial

triggers:
- name: gas_leak_detected
Expand All @@ -35,6 +37,18 @@ triggers:
escalate_to: local_slm
action_tier: A

# Tier C — Compressor overload against a known baseline: requires operator approval.
# baseline_current and compressor_fault_active are injected by hooks at commissioning
# after the technician measures and records the unit's normal draw.
# sensor_id guard is needed because the demo evaluates all current readings against
# this skill; in production only HVAC sensors are routed here.
- name: compressor_overload
condition: "(sensor_type == 'current_clamp' or sensor_type == 'current') and sensor_id == 'ac-01_compressor_current' and compressor_fault_active == 1 and value >= baseline_current * compressor_overload_ratio"
action_tier: C
requires_approval: true
escalate_to: rule
cooldown_seconds: 0

prompts:
gas_concentration: |
Gas concentration reading: {value} ppm
Expand All @@ -46,6 +60,13 @@ prompts:
What immediate checks should the HVAC technician perform?
Answer in 2-3 plain sentences.

compressor_overload: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
the AC compressor current is elevated and what it may indicate.
Do not include timestamps, currency, percentages, units, or action statements.
Example: "The air conditioner compressor is drawing more power than expected, which can signal mechanical wear."

current_clamp: |
Compressor current reading: {value} A
24-hour baseline: {history.avg_24h('load_current')} A
Expand All @@ -71,3 +92,10 @@ actions:
gas_leak_detected: [close_gas_valve, alert_whatsapp, alert_sms, log_to_dashboard]
gas_concentration_elevated: [alert_whatsapp, log_to_dashboard]
compressor_current_anomaly: [alert_whatsapp, log_to_dashboard]
compressor_overload: [alert_whatsapp, log_to_dashboard]

config:
# compressor_overload trigger — set by hooks at commissioning after measuring baseline
compressor_fault_active: 0
baseline_current: 5.0
compressor_overload_ratio: 1.4
24 changes: 24 additions & 0 deletions skills/solar-performance-monitor/skill.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ sensors_required:
protocol: victron
- type: victron_battery_soc
protocol: victron
- type: power
protocol: simulation

triggers:
- name: solar_underperforming_daytime
Expand All @@ -34,13 +36,30 @@ triggers:
escalate_to: local_slm
action_tier: A

# Tier A — Direct performance-ratio check for simulation and inverter power sensors.
# expected_output_watts and performance_ratio are forwarded from reading.metadata
# (set by the solar device simulator or real inverter telemetry).
# expected_output_watts: 0.0 default suppresses night-time firing.
- name: solar_output_degraded
condition: "(sensor_type == 'power' or sensor_type == 'growatt_pv_power' or sensor_type == 'victron_pv_power') and expected_output_watts > 0 and performance_ratio < solar_min_performance_ratio"
action_tier: A
escalate_to: local_slm
cooldown_seconds: 0

- name: unexpected_grid_draw_with_good_sun
condition: "(sensor_type == 'growatt_grid_power' or sensor_type == 'victron_grid_power') and quality >= min_quality and config_valid == 1 and is_daytime == 1 and pv_ratio_snapshot >= strong_sun_ratio and grid_import_watts >= grid_draw_threshold_watts"
cooldown_seconds: 900
escalate_to: local_slm
action_tier: A

prompts:
solar_output_degraded: |
Your job is diagnosis only.
Write one plain sentence for a non-technical building owner explaining why
solar output is below expectation and what typically causes it.
Do not include timestamps, currency, percentages, units, or action statements.
Example: "Your solar panels are producing less power than expected, which is often caused by dust or shading."

solar_underperforming_daytime: |
Your job is diagnosis only.
Write one plain sentence for a non-technical business owner explaining the
Expand Down Expand Up @@ -72,6 +91,7 @@ actions:
- name: log_to_dashboard
tier: A
defaults:
solar_output_degraded: [alert_whatsapp, log_to_dashboard]
solar_underperforming_daytime: [alert_whatsapp, log_to_dashboard]
battery_not_charging_when_pv_available: [alert_whatsapp, log_to_dashboard]
unexpected_grid_draw_with_good_sun: [alert_whatsapp, log_to_dashboard]
Expand All @@ -92,3 +112,7 @@ config:
strong_sun_ratio: 0.5
grid_draw_threshold_watts: 500.0
battery_not_full_soc_threshold: 95.0
# solar_output_degraded trigger — forwarded from reading.metadata per tick
solar_min_performance_ratio: 0.75
expected_output_watts: 0.0
performance_ratio: 1.0
4 changes: 2 additions & 2 deletions tests/test_battery_lifecycle_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ def _ctx(skill, event, store):
async def test_skill_loads_with_expected_triggers():
skill = _load_skill()
assert skill.name == "battery-lifecycle-observer"
assert len(skill.triggers) == 3
assert {t.action_tier for t in skill.triggers} == {"A"}
assert len(skill.triggers) == 4
assert {t.action_tier for t in skill.triggers} == {"A", "D"}


@pytest.mark.asyncio
Expand Down
4 changes: 2 additions & 2 deletions tests/test_energy_anomaly_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ def _seed_history(store: _Store, sensor_id: str, values: list[float]) -> None:
async def test_skill_loads_with_v2_triggers():
skill = _load_skill()
assert skill.name == "energy-anomaly-detector"
assert len(skill.triggers) == 4
assert {trigger.action_tier for trigger in skill.triggers} == {"A", "D"}
assert len(skill.triggers) == 6
assert {trigger.action_tier for trigger in skill.triggers} == {"A", "D", "B"}


def test_hook_computes_baseline_and_deviation():
Expand Down
2 changes: 1 addition & 1 deletion tests/test_hvac_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _load_skill():
def test_skill_loads():
skill = _load_skill()
assert skill.name == "hvac-refrigerant-monitor"
assert len(skill.triggers) == 3
assert len(skill.triggers) == 4


def test_gas_leak_trigger_is_tier_d():
Expand Down
5 changes: 4 additions & 1 deletion tests/test_integration_rule_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ async def test_dangerous_overcurrent_returns_tier_d_with_proof_fields() -> None:
assert result.action_tier == "D"
assert result.trigger_name == "dangerous_overcurrent"
assert result.bypass_llm is True
assert result.proof_rule_condition == "value > dangerous_overcurrent_threshold"
assert (
result.proof_rule_condition
== "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and value > dangerous_overcurrent_threshold"
)
assert result.proof_threshold_name == "dangerous_overcurrent_threshold"
assert result.proof_threshold == pytest.approx(20.0)
assert result.proof_sensor_value == pytest.approx(52.0)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_solar_performance_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _ctx(skill, event, store):
async def test_skill_loads_with_expected_triggers():
skill = _load_skill()
assert skill.name == "solar-performance-monitor"
assert len(skill.triggers) == 3
assert len(skill.triggers) == 4
assert {t.action_tier for t in skill.triggers} == {"A"}


Expand Down