From f4be9ffb176390e56ea9beae0eeffc4861086a44 Mon Sep 17 00:00:00 2001 From: Wasiu Bakare Date: Tue, 16 Jun 2026 16:44:39 +0100 Subject: [PATCH 1/3] skill: add demo simulation triggers to four bundled skills The demo-simulation package proves the Ori runtime decision stack by evaluating simulated Lagos building sensor readings through ori.integration. The four bundled energy skills were missing the triggers needed to cover all seven demo scenarios, so the demo had to ship its own temporary skill YAML. This commit adds those triggers to the correct first-party skills so the demo can run against the real bundled stack with skills_root=None: energy-anomaly-detector: - Fix dangerous_overcurrent condition: add sensor_type guard so voltage and other non-current readings cannot false-fire the Tier D path - Add voltage_overvoltage (Tier D) for grid overvoltage events (>260V) - Add grid_phcn_restored (Tier B) for grid restoration detection; phcn_restore_active flag defaults to 0 and is injected by hooks when load is on generator, preventing false fires on deployments without live context battery-lifecycle-observer: - Add battery_emergency_cutoff (Tier D) for critical SOC (<5%); covers sensor_type='battery' (simulation), Growatt, Victron, and hook-derived is_soc_sensor so the same trigger works across all supported inverters hvac-refrigerant-monitor: - Add compressor_overload (Tier C, requires_approval) for overcurrent against a commissioning-measured baseline; compressor_fault_active flag defaults to 0 and is set by hooks after the technician records the baseline draw solar-performance-monitor: - Add solar_output_degraded (Tier A) using performance_ratio forwarded from reading metadata; expected_output_watts defaults to 0.0 to suppress night-time firing without a live StateStore --- skills/battery-lifecycle-observer/skill.yaml | 23 ++++++++++ skills/energy-anomaly-detector/skill.yaml | 47 +++++++++++++++++++- skills/hvac-refrigerant-monitor/skill.yaml | 28 ++++++++++++ skills/solar-performance-monitor/skill.yaml | 24 ++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) diff --git a/skills/battery-lifecycle-observer/skill.yaml b/skills/battery-lifecycle-observer/skill.yaml index 2bf18ac..83b44bb 100644 --- a/skills/battery-lifecycle-observer/skill.yaml +++ b/skills/battery-lifecycle-observer/skill.yaml @@ -18,6 +18,8 @@ sensors_required: protocol: growatt - type: victron_grid_power protocol: victron + - type: battery + protocol: simulation triggers: - name: battery_deep_discharge_risk @@ -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 @@ -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 @@ -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 @@ -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"] diff --git a/skills/energy-anomaly-detector/skill.yaml b/skills/energy-anomaly-detector/skill.yaml index 1cda1b2..6fc23ec 100644 --- a/skills/energy-anomaly-detector/skill.yaml +++ b/skills/energy-anomaly-detector/skill.yaml @@ -11,6 +11,8 @@ sensors_required: protocol: i2c - type: current protocol: serial + - type: voltage + protocol: serial triggers: - name: sustained_overdraw @@ -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. @@ -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] @@ -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 @@ -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 diff --git a/skills/hvac-refrigerant-monitor/skill.yaml b/skills/hvac-refrigerant-monitor/skill.yaml index deb71fe..f31115d 100644 --- a/skills/hvac-refrigerant-monitor/skill.yaml +++ b/skills/hvac-refrigerant-monitor/skill.yaml @@ -15,6 +15,8 @@ sensors_required: protocol: i2c - type: current_clamp protocol: i2c + - type: current + protocol: serial triggers: - name: gas_leak_detected @@ -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 @@ -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 @@ -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 diff --git a/skills/solar-performance-monitor/skill.yaml b/skills/solar-performance-monitor/skill.yaml index 38fe0d6..8662558 100644 --- a/skills/solar-performance-monitor/skill.yaml +++ b/skills/solar-performance-monitor/skill.yaml @@ -20,6 +20,8 @@ sensors_required: protocol: victron - type: victron_battery_soc protocol: victron + - type: power + protocol: simulation triggers: - name: solar_underperforming_daytime @@ -34,6 +36,16 @@ 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 @@ -41,6 +53,13 @@ triggers: 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 @@ -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] @@ -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 From f14c9b5cbca3cdb05b14d96fcee17a0d98b82b08 Mon Sep 17 00:00:00 2001 From: Wasiu Bakare Date: Tue, 16 Jun 2026 16:52:36 +0100 Subject: [PATCH 2/3] test: update skill trigger counts and condition strings for new triggers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five tests hardcoded trigger counts and condition strings that no longer match after the demo simulation triggers were added in the previous commit. - test_battery_lifecycle_skill: 3 → 4 triggers; tiers now {"A", "D"} - test_energy_anomaly_skill: 4 → 6 triggers; tiers now {"A", "D", "B"} - test_hvac_skill: 3 → 4 triggers - test_solar_performance_skill: 3 → 4 triggers; tier set stays {"A"} - test_integration_rule_evaluation: update proof_rule_condition string to match the sensor_type guard added to dangerous_overcurrent --- tests/test_battery_lifecycle_skill.py | 4 ++-- tests/test_energy_anomaly_skill.py | 4 ++-- tests/test_hvac_skill.py | 2 +- tests/test_integration_rule_evaluation.py | 2 +- tests/test_solar_performance_skill.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_battery_lifecycle_skill.py b/tests/test_battery_lifecycle_skill.py index ec28cd5..422b12f 100644 --- a/tests/test_battery_lifecycle_skill.py +++ b/tests/test_battery_lifecycle_skill.py @@ -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 diff --git a/tests/test_energy_anomaly_skill.py b/tests/test_energy_anomaly_skill.py index 3de1875..5dbb422 100644 --- a/tests/test_energy_anomaly_skill.py +++ b/tests/test_energy_anomaly_skill.py @@ -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(): diff --git a/tests/test_hvac_skill.py b/tests/test_hvac_skill.py index 0f7b3e3..6d61e9c 100644 --- a/tests/test_hvac_skill.py +++ b/tests/test_hvac_skill.py @@ -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(): diff --git a/tests/test_integration_rule_evaluation.py b/tests/test_integration_rule_evaluation.py index fa2228a..c8bc484 100644 --- a/tests/test_integration_rule_evaluation.py +++ b/tests/test_integration_rule_evaluation.py @@ -79,7 +79,7 @@ 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) diff --git a/tests/test_solar_performance_skill.py b/tests/test_solar_performance_skill.py index f3770e3..ca3386d 100644 --- a/tests/test_solar_performance_skill.py +++ b/tests/test_solar_performance_skill.py @@ -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"} From 8d7e77ead90e9f0b302d710cdf01e38186417655 Mon Sep 17 00:00:00 2001 From: Wasiu Bakare Date: Tue, 16 Jun 2026 16:56:27 +0100 Subject: [PATCH 3/3] style: wrap long proof_rule_condition assertion for ruff-format --- tests/test_integration_rule_evaluation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_integration_rule_evaluation.py b/tests/test_integration_rule_evaluation.py index c8bc484..2d21dd1 100644 --- a/tests/test_integration_rule_evaluation.py +++ b/tests/test_integration_rule_evaluation.py @@ -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 == "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and 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)