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
119 changes: 119 additions & 0 deletions docs/phone-termux-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Ori Phone Termux Path

This document defines the phone-first Ori Energy wedge. The phone path is a
low-friction deployment profile for early African SME sites, not the final
hardware architecture for physical actuation.

## Product Boundary

The phone path turns a spare Android phone into an Ori edge runtime for Tier A
energy intelligence:

- read a USB serial energy meter through Android USB OTG;
- build a site-local baseline for power draw;
- detect sustained overdraw, sudden spikes, and unstable draw;
- send operator alerts and dashboard logs;
- export consented telemetry to ori-cloud for fleet intelligence.

The phone path does not provide certified relay control. Tier B/C physical
switching, load separation, generator/grid transfer, and equipment cutoffs
require a certified Ori Edge Node installation. Tier D safety semantics remain
inviolable, but a phone-only deployment has no direct relay authority unless a
separate certified actuator path is installed.

## Onboarding Kit

The basic phone onboarding kit contains:

- an Android phone with Termux installed, supplied by the customer or bundled by
Ori;
- a PZEM-004T or equivalent USB/Modbus energy meter;
- a USB OTG adapter cable;
- a short setup card with the Ori install command and support contact.

The USB energy meter is the small sensor device. It measures voltage, current,
power, energy, frequency, and power factor in hardware. Termux does not install
software onto the meter; Termux runs the Ori runtime, and the runtime's
`usb_serial` HAL reads the meter over the phone's USB port.

Customer-facing copy should say that the meter "connects to your phone's USB
port using a small adapter cable." Do not imply that the phone's charging port
directly senses the building's mains supply.

## Runtime Shape

Use `ori.yaml.phone.example` as the starting profile:

- `device.deployment_type: phone`;
- `sensors[].protocol: usb_serial`;
- `sensors[].type: usb_power` for a single plug-level phone starter sensor;
- `actions.relay.enabled: false`;
- `gateway.enabled: false` unless the phone is explicitly bridged to a local
gateway.

The `energy-anomaly-detector` skill accepts `usb_power`, `usb_current`, and
`usb_voltage`. For `usb_power`, the hook treats the reading as watts and uses it
directly for cost projection. It does not reinterpret watts as amps.

## PWA Role

The PWA is an operator and commercial surface, not the sensing runtime. It can
show onboarding status, live site health, alerts, invoices, subscription state,
reports, and upgrade prompts from any modern phone, including iPhone.

The Android Termux runtime remains the edge execution path because browsers and
PWAs cannot reliably read USB serial meters, run offline background loops, or
enforce Ori's runtime safety model.

## Device Policy On Phone

The phone runtime uses the same DevicePolicy concept as Pi deployments:

- active trial or paid Starter Phone tier permits Tier A intelligence and
telemetry export;
- expired subscription restricts non-safety business features such as premium
reports, cloud sync, and advanced reasoning;
- Tier D safety behavior is never disabled by subscription state;
- relay entitlements are always false for phone-only deployments.

Phone deployments need a stable device identity from the cloud registration
flow. Reinstalling Termux must not silently create a fresh paid trial for the
same site. ori-cloud should bind the phone runtime to a site, account, and
hardware fingerprint where available, while still allowing support-led recovery
when a phone is replaced.

## Telemetry And Data Moat

The phone should keep local readings available offline, but consented telemetry
must not remain trapped on the handset. The runtime should export:

- normalized sensor readings at a cloud-controlled sampling rate;
- alert and action logs;
- baseline summaries and derived anomaly features;
- device health and sync status.

Sensitive historical exports must follow the runtime-gateway/cloud security
posture: authenticated envelopes, replay protection, and encryption for
business/audit history when gateway encryption is enabled.

## ori-cloud Requirements

ori-cloud needs explicit support for phone deployments:

- a `phone_starter` or equivalent subscription tier mapped to Tier A energy
intelligence, no relay entitlement, and limited local/cloud reasoning;
- device registration that issues runtime credentials, records deployment type
`phone`, and binds the runtime to account, site, and subscription;
- DevicePolicy generation for phone runtimes, including trial expiry,
subscription status, telemetry allowance, report allowance, and relay
prohibition;
- telemetry ingestion endpoints for phone runtime exports with consent, rate
limits, idempotency, and offline backfill handling;
- a PWA dashboard that reads cloud state instead of talking directly to the USB
meter;
- upgrade flow from Phone Starter to Certified Edge Node without losing the
site's historical baseline.

The `/Users/adegneus/Ori-Platform/ori-energy` demo should be treated as the
product proxy for this flow: waitlist, onboarding, site health, alerts,
subscription state, and the Edge Node upgrade path.
21 changes: 18 additions & 3 deletions ori.yaml.phone.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
# ori.yaml.phone.example
# Phone-as-Gateway deployment profile (Termux on Android)
#
# This profile is the Ori Energy phone wedge: an Android phone runs the runtime
# in Termux and reads a USB serial energy meter through an OTG adapter. The
# phone is an edge runtime and gateway substitute for Tier A intelligence; it is
# not a substitute for certified Edge Node relay wiring.

device:
id: phone-gateway-ikeja-01
name: Ikeja Office Phone Gateway
location: Lagos, Nigeria
timezone: Africa/Lagos
country_code: NG
deployment_type: phone
site_type: office

sensors:
- id: mains-power
# PZEM-004T or equivalent USB/Modbus energy meter connected to the Android
# phone's USB port through an OTG adapter. The USB meter is the small sensor
# device in the phone onboarding kit; Termux only runs the Ori runtime.
- id: phone-main-power
type: usb_power
protocol: usb_serial
device_path: /dev/ttyUSB0
Expand All @@ -18,8 +28,11 @@ sensors:

skills:
- name: energy-anomaly-detector
version: "0.2.1"
config: {}
version: "0.1.0"
config:
country_code: NG
tariff_per_kwh: 225.0
currency_code: NGN

reasoning:
default_tier: local
Expand All @@ -40,6 +53,8 @@ actions:
sms:
enabled: false
relay:
# Phone deployments must keep relay disabled. Tier B/C physical actuation
# requires a certified Ori Edge Node installation.
enabled: false
gpio_pin: 26

Expand Down
35 changes: 27 additions & 8 deletions skills/energy-anomaly-detector/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ def _stddev(values):
"CA": 120.0,
}

_POWER_SENSOR_TYPES = frozenset({"usb_power"})


def _resolve_timezone(tz_name):
return resolve_timezone(tz_name)
Expand Down Expand Up @@ -255,7 +257,9 @@ def pre_trigger_eval(context):
context.derived["sustained_high_ratio"] = 0.0
context.derived["sustained_high_count"] = 0
context.derived["recent_volatility_percent"] = 0.0
context.derived["measurement_kind"] = "current"
context.derived["delta_amps"] = 0.0
context.derived["delta_watts"] = 0.0
context.derived["line_voltage_used"] = 0.0
context.derived["power_factor_used"] = 0.0
context.derived["estimated_kw_delta"] = 0.0
Expand All @@ -269,6 +273,9 @@ def pre_trigger_eval(context):
return context

sensor_id = str(getattr(reading, "sensor_id", "")).strip()
sensor_type = str(getattr(reading, "sensor_type", "")).strip()
measurement_kind = "power" if sensor_type in _POWER_SENSOR_TYPES else "current"
context.derived["measurement_kind"] = measurement_kind
current_value = as_float(getattr(reading, "value", 0.0), 0.0)
history_window = context.derived["history_window"]
persistence_window = context.derived["persistence_window"]
Expand Down Expand Up @@ -339,12 +346,17 @@ def pre_trigger_eval(context):
recent_values
)

# Deterministic cost estimator (P3-R6):
# keep both observed window and projected /day run-rate values.
# Deterministic cost estimator: USB power meters report watts
# directly, while clamp/current sensors need voltage and power factor.
delta_amps = 0.0
delta_watts = 0.0
if baseline_valid == 1 and current_value > baseline_24h:
delta_amps = current_value - baseline_24h
if measurement_kind == "power":
delta_watts = current_value - baseline_24h
else:
delta_amps = current_value - baseline_24h
context.derived["delta_amps"] = delta_amps
context.derived["delta_watts"] = delta_watts

tariff_per_kwh, tariff_is_explicit = _resolve_tariff(context)
line_voltage, voltage_confidence = _resolve_line_voltage(context)
Expand All @@ -353,15 +365,22 @@ def pre_trigger_eval(context):
context.derived["line_voltage_used"] = line_voltage
context.derived["power_factor_used"] = power_factor

confidence = (
"exact" if tariff_is_explicit and voltage_confidence == "exact" else "estimated"
)
confidence = "estimated"
if tariff_is_explicit:
if measurement_kind == "power" or voltage_confidence == "exact":
confidence = "exact"
context.derived["cost_confidence"] = confidence

if delta_amps <= 0.0 or tariff_per_kwh <= 0.0:
if measurement_kind == "power":
kw_delta = delta_watts / 1000.0
else:
kw_delta = (delta_amps * line_voltage * power_factor) / 1000.0
delta_watts = delta_amps * line_voltage * power_factor
context.derived["delta_watts"] = delta_watts

if kw_delta <= 0.0 or tariff_per_kwh <= 0.0:
return context

kw_delta = (delta_amps * line_voltage * power_factor) / 1000.0
context.derived["estimated_kw_delta"] = kw_delta

observed_hours = 0.0
Expand Down
18 changes: 12 additions & 6 deletions skills/energy-anomaly-detector/skill.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,41 @@ sensors_required:
protocol: serial
- type: voltage
protocol: serial
- type: usb_power
protocol: usb_serial
- type: usb_current
protocol: usb_serial
- type: usb_voltage
protocol: usb_serial

triggers:
- name: sustained_overdraw
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and deviation_percent >= overdraw_threshold_percent and sustained_high_ratio >= sustained_ratio_threshold"
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current' or sensor_type == 'usb_current' or sensor_type == 'usb_power') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and deviation_percent >= overdraw_threshold_percent and sustained_high_ratio >= sustained_ratio_threshold"
cooldown_seconds: 600
escalate_to: local_slm
action_tier: A

- name: sudden_load_spike
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and spike_ratio >= spike_ratio_threshold and deviation_percent >= (overdraw_threshold_percent / 2.0)"
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current' or sensor_type == 'usb_current' or sensor_type == 'usb_power') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and spike_ratio >= spike_ratio_threshold and deviation_percent >= (overdraw_threshold_percent / 2.0)"
cooldown_seconds: 300
escalate_to: local_slm
action_tier: A

- name: unstable_power_draw
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and recent_volatility_percent >= volatility_threshold_percent and deviation_percent >= (overdraw_threshold_percent / 3.0)"
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current' or sensor_type == 'usb_current' or sensor_type == 'usb_power') and quality >= min_quality and baseline_valid == 1 and context_aware_suppression == 0 and recent_volatility_percent >= volatility_threshold_percent and deviation_percent >= (overdraw_threshold_percent / 3.0)"
cooldown_seconds: 600
escalate_to: local_slm
action_tier: A

- name: dangerous_overcurrent
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current') and value > dangerous_overcurrent_threshold"
condition: "(sensor_type == 'current_clamp' or sensor_type == 'ads1115_current' or sensor_type == 'current' or sensor_type == 'usb_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"
condition: "(sensor_type == 'voltage' or sensor_type == 'usb_voltage') and sensor_id == 'grid-main_voltage' and value > overvoltage_threshold"
bypass_llm: true
action_tier: D
cooldown_seconds: 0
Expand All @@ -50,7 +56,7 @@ triggers:
# 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"
condition: "(sensor_type == 'voltage' or sensor_type == 'usb_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
Expand Down
Loading