File size: 5,190 Bytes
31226fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""Explicit, fail-closed lifecycle control for registered sensors."""

from __future__ import annotations

from dataclasses import dataclass
from time import time_ns
from typing import Callable

from .connections import ConnectionDescriptor, ConnectionKind, ConnectionManager


def _allow_sensor(_descriptor: ConnectionDescriptor) -> bool:
    """Default policy used only when no stricter caller policy is supplied."""
    return True


@dataclass(frozen=True, slots=True)
class SensorActivationAudit:
    """One requested sensor transition, including rejected requests."""

    tick: int
    timestamp_ns: int
    connection_id: str
    requested_state: str
    previous_state: str
    new_state: str
    operator_source: str
    result: str
    error: str | None

    def to_json(self) -> dict[str, object]:
        return {
            "tick": self.tick,
            "timestamp_ns": self.timestamp_ns,
            "connection_id": self.connection_id,
            "requested_state": self.requested_state,
            "previous_state": self.previous_state,
            "new_state": self.new_state,
            "operator_source": self.operator_source,
            "result": self.result,
            "error": self.error,
        }


class SensorActivationService:
    """Control sensor activation without creating a second connection registry."""

    def __init__(
        self,
        connections: ConnectionManager,
        *,
        safety_policy: Callable[[ConnectionDescriptor], bool] | None = None,
    ) -> None:
        self.connections = connections
        self.safety_policy: Callable[[ConnectionDescriptor], bool] = (
            safety_policy or _allow_sensor
        )
        self._audit: list[SensorActivationAudit] = []

    @property
    def audit(self) -> tuple[SensorActivationAudit, ...]:
        return tuple(self._audit)

    def sensors(self) -> tuple[ConnectionDescriptor, ...]:
        return tuple(
            item
            for item in self.connections.snapshot()
            if item.kind == ConnectionKind.SENSOR
        )

    def get(self, connection_id: str) -> ConnectionDescriptor | None:
        descriptor = self.connections.get(connection_id)
        return (
            descriptor
            if descriptor is not None and descriptor.kind == ConnectionKind.SENSOR
            else None
        )

    def set_enabled(
        self,
        connection_id: str,
        enabled: bool,
        *,
        tick: int = 0,
        operator_source: str = "dashboard",
    ) -> tuple[ConnectionDescriptor, SensorActivationAudit]:
        current = self.get(connection_id)
        if current is None:
            raise KeyError(connection_id)
        previous_state = (
            "ACTIVE"
            if current.active
            else ("DISABLED" if current.enabled else current.health)
        )
        requested_state = "ACTIVE" if enabled else "DISABLED"
        error: str | None = None
        result = "accepted"
        updated = current
        if enabled:
            if not current.available:
                error = "adapter unavailable: sensor was not detected"
            elif not current.configured or not current.adapter_id:
                error = "adapter unavailable: no configured sensor adapter"
            elif not current.authorized:
                error = "authorization required before sensor activation"
            elif not self.safety_policy(current):
                error = "safety policy denied sensor activation"
            if error is not None:
                result = "rejected"
                updated = self.connections.update(
                    connection_id,
                    active=False,
                    enabled=False,
                    health=(
                        current.health
                        if current.health != "UNAVAILABLE"
                        else "UNAVAILABLE"
                    ),
                    last_error=error,
                    message=error,
                )
            else:
                updated = self.connections.update(
                    connection_id,
                    active=True,
                    enabled=True,
                    status=current.status,
                    health="ACTIVE",
                    last_error=None,
                    message="Sensor adapter active.",
                )
        else:
            updated = self.connections.update(
                connection_id,
                active=False,
                enabled=False,
                health="DISABLED",
                last_error=None,
                message="Sensor activation disabled by operator.",
            )
        audit = SensorActivationAudit(
            tick=tick,
            timestamp_ns=time_ns(),
            connection_id=connection_id,
            requested_state=requested_state,
            previous_state=previous_state,
            new_state="ACTIVE" if updated.active else updated.health,
            operator_source=operator_source,
            result=result,
            error=error,
        )
        self._audit.append(audit)
        return updated, audit


__all__ = ["SensorActivationAudit", "SensorActivationService"]