"""Authenticated data onboarding and observed workflow, never actuator authority.""" from datetime import timedelta from uuid import UUID import json from . import measurement_pipeline from .domain import utc from .receiver_contract import control_context REPORT_MAX_AGE_SECONDS = 120 def canonical(value): return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False) def schema(con): con.executescript(''' CREATE TABLE IF NOT EXISTS planner_enrollment( plant TEXT PRIMARY KEY, dataset TEXT NOT NULL, learning_enabled INTEGER NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS planner_workflow_report( plant TEXT PRIMARY KEY, observed_at TEXT NOT NULL, received_at TEXT NOT NULL, value TEXT NOT NULL); ''') def identity(plant, value): if not isinstance(plant, str) or str(UUID(plant)) != plant: raise ValueError('Canonical installation UUID required') if type(value.get('version')) is not int or value['version'] != 1 or value.get('installationId') != plant: raise ValueError('Installation path/payload mismatch or unsupported version') def enrolled(con, plant): return con.execute('SELECT * FROM planner_enrollment WHERE plant=?', (plant,)).fetchone() def learning_enabled(con, plant): row = enrolled(con, plant) if row is not None: return bool(row['learning_enabled']) report_row = con.execute('SELECT value FROM planner_workflow_report WHERE plant=?', (plant,)).fetchone() return report_row is None or json.loads(report_row['value'])['learningEnabled'] def setup(store, plant, value, now): identity(plant, value) if set(value) != {'version', 'installationId', 'dataset', 'learningEnabled'} or type(value['learningEnabled']) is not bool: raise ValueError('Explicit dataset and boolean learningEnabled required') config = measurement_pipeline.validate_config(value['dataset']) # Compatibility proofs and relaxed history-timing policies remain operator-only. existing = store.con.execute('SELECT config FROM planner_data_sets WHERE plant=? AND dataset=?', (plant, config['datasetId'])).fetchone() if not existing and (config.get('sourceDatasetId') or config.get('historyTimingPolicy')): raise ValueError('Device setup cannot grant history compatibility or timing exceptions') if not existing and (config['minimumCoverage'] < .95 or config['maximumGapSeconds'] > 5 or config['minimumTrainingHours'] < 24): raise ValueError('New datasets require coverage >= .95, gap <= 5s and training >= 24h') con = store.con con.execute('BEGIN IMMEDIATE') try: measurement_pipeline.register_dataset(con, plant, config, int(now.timestamp()), own_transaction=False) current = store.settings(plant) initialized = con.execute('SELECT 1 FROM planner_settings WHERE plant=?', (plant,)).fetchone() is None if initialized: # Native interval estimates retain explicit provenance and gap checks; # this is a planning policy, never billing or actuator evidence. current.update(forecastSource='corrected_profile', measurementDataset=config['datasetId'], trainingCadence='daily', measurementPolicy='allow_estimates') current.pop('revision') store._validate_settings(current) con.execute('INSERT INTO planner_settings VALUES(?,?,?)', (plant, 1, canonical(current))) store._request(plant, 1, 'onboarding', now) con.execute('INSERT INTO planner_audit(plant,at,kind,detail) VALUES(?,?,?,?)', (plant, utc(now).isoformat(), 'onboarding', canonical({'datasetId': config['datasetId'], 'controlEnabled': False}))) stamp = utc(now).isoformat() con.execute('''INSERT INTO planner_enrollment VALUES(?,?,?,?,?) ON CONFLICT(plant) DO UPDATE SET dataset=excluded.dataset, learning_enabled=excluded.learning_enabled,updated_at=excluded.updated_at''', (plant, config['datasetId'], int(value['learningEnabled']), stamp, stamp)) con.commit() except Exception: con.rollback() raise settings = store.settings(plant) return {'status': 'configured', 'installationId': plant, 'datasetId': config['datasetId'], 'selectedDatasetId': settings['measurementDataset'], 'settingsInitialized': initialized, 'settingsRevision': settings['revision'], 'learningEnabled': value['learningEnabled'], 'controlEnabled': False, 'controlledTrialAuthorized': False} def report(store, plant, value, now): identity(plant, value) if set(value) != {'version', 'installationId', 'observedAt', 'learningEnabled', 'controlRequested', 'controlActive', 'reason'}: raise ValueError('Explicit workflow report required') for key in ('learningEnabled', 'controlRequested', 'controlActive'): if type(value[key]) is not bool: raise ValueError('Workflow flags must be boolean') reason = value['reason'] if not isinstance(reason, str) or len(reason) > 300 or any(ord(c) < 32 for c in reason): raise ValueError('Workflow reason must be plain text up to 300 characters') observed = measurement_pipeline.epoch(value['observedAt']) if not -30 <= now.timestamp() - observed <= REPORT_MAX_AGE_SECONDS: raise ValueError('Fresh whole-second UTC workflow report required') if value['controlActive'] and not (value['learningEnabled'] and value['controlRequested']): raise ValueError('Active control requires learning and requested control') normalized = {**value, 'observedAt': measurement_pipeline.iso(observed)} data = canonical(normalized) con = store.con con.execute('BEGIN IMMEDIATE') try: old = con.execute('SELECT observed_at,value FROM planner_workflow_report WHERE plant=?', (plant,)).fetchone() if old and utc(old['observed_at']).timestamp() > observed: raise ValueError('Workflow report moved backwards') if old and utc(old['observed_at']).timestamp() == observed: if old['value'] != data: raise ValueError('Conflicting workflow report at the same time') con.commit() return {'status': 'duplicate', 'controlEnabled': False} con.execute('''INSERT INTO planner_workflow_report VALUES(?,?,?,?) ON CONFLICT(plant) DO UPDATE SET observed_at=excluded.observed_at, received_at=excluded.received_at,value=excluded.value''', (plant, normalized['observedAt'], utc(now).isoformat(), data)) # The authenticated first switch also pauses autonomous training. A stop # remains effective after report expiry; stale telemetry cannot re-enable it. registration = enrolled(con, plant) if registration and observed >= int(utc(registration['updated_at']).timestamp()): con.execute('UPDATE planner_enrollment SET learning_enabled=?,updated_at=? WHERE plant=?', (int(value['learningEnabled']), utc(now).isoformat(), plant)) con.commit() except Exception: con.rollback() raise return {'status': 'recorded', 'controlEnabled': False} def planning_continuity(store, plant, state, now): """Keep readiness during routine telemetry replans, never renew a receipt. This does not change receiver ``fresh``. The Manager must still validate its originally accepted envelope and stop when that local envelope expires. """ if state['fresh']: return True try: pending = state['pending'] plan = state['plan'] settings = state['settings'] run = state['lastRun'] if not pending or not plan or not run: return False reasons = json.loads(pending['reasons']) if not isinstance(reasons, list) or not reasons or not set(reasons) <= {'operation_changed', 'telemetry_changed', 'five_minute_tick'}: return False revision = settings['revision'] if pending['revision'] != revision or plan['configRevision'] != revision: return False if run['status'] not in ('optimal', 'feasible_time_limit') or run['detail'].get('planId') != plan['planId']: return False if run['detail'].get('configRevision') != revision: return False if not utc(plan['validFrom']) <= now < utc(plan['validUntil']) or not 0 <= (now-utc(plan['generatedAt'])).total_seconds() <= 900: return False rows = store.con.execute('''SELECT i.kind,i.value FROM planner_inputs i JOIN planner_input_current c ON i.plant=c.plant AND i.kind=c.kind AND i.event_id=c.event_id WHERE i.plant=? AND i.kind IN ('operation','forecast','tariffs')''', (plant,)) inputs = {row['kind']: json.loads(row['value']) for row in rows} if any(plan['inputRefs'][kind] != inputs[kind]['eventId'] for kind in ('forecast', 'tariffs')): return False operation = inputs['operation'] if not 0 <= (now-utc(operation['observedAt'])).total_seconds() <= 120: return False return control_context(operation) == plan['controlContext'] except (KeyError, TypeError, ValueError, AttributeError): return False def view(store, plant, state, now): settings = state['settings'] registration = enrolled(store.con, plant) row = store.con.execute('SELECT value FROM planner_workflow_report WHERE plant=?', (plant,)).fetchone() report_value = json.loads(row['value']) if row else None source = utc(report_value['observedAt']) if report_value else None report_fresh = bool(source and 0 <= (now-source).total_seconds() <= REPORT_MAX_AGE_SECONDS) learning = bool(registration['learning_enabled']) if registration else bool(report_fresh and report_value['learningEnabled']) requested = bool(report_value and report_value['controlRequested']) dataset = settings['measurementDataset'] model = measurement_pipeline.current_model(store.con, plant, dataset, int(now.timestamp())) if dataset else None pipeline = next((d for d in state['dataPipeline']['datasets'] if d['datasetId'] == dataset), {}) plan = state.get('plan') or {} selected = plan.get('inputQuality', {}).get('dataPipeline', {}) ready = bool(settings['forecastSource'] == 'corrected_profile' and model and planning_continuity(store, plant, state, now) and plan.get('executable') is True and selected.get('datasetId') == dataset and selected.get('modelId') == model['modelId']) active = bool(learning and requested and report_fresh and report_value['controlActive'] and ready) if active: name, reason = 'active', report_value['reason'] or 'manager_control_active' elif requested: name = 'interrupted' reason = 'manager_report_stale' if not report_fresh else report_value['reason'] or ('planning_not_ready' if not ready else 'manager_control_not_active') elif not learning: name, reason = 'disabled', 'learning_disabled' elif ready: name, reason = 'ready', 'plan_and_model_ready' elif model: name = 'learning' reason = (state.get('lastRun') or {}).get('detail', {}).get('reason') or 'awaiting_fresh_plan' elif pipeline.get('status') in ('training', 'candidate_pending'): name, reason = 'learning', 'model_training' else: name, reason = 'collecting', 'collecting_measurements' if registration else 'setup_required' return {'state': name, 'reason': reason, 'learningEnabled': learning, 'controlRequested': requested, 'controlActive': active, 'planningReady': ready, 'checkedAt': utc(now).isoformat(), 'sourceReportAt': source.isoformat() if source else None, 'sourceFreshUntil': (source+timedelta(seconds=REPORT_MAX_AGE_SECONDS)).isoformat() if source else None, 'reportFresh': report_fresh, 'usingPreviousPlan': bool(ready and not state['fresh'])}