from dataclasses import dataclass
from math import isfinite
from typing import Protocol
import unittest
import io

@dataclass(frozen=True)
class Meting:
    waarde: float
    tijd_s: float
    eenheid: str = "degC"

class Sensor(Protocol):
    def lees(self) -> Meting: ...

class ProefSensor:
    def __init__(self, waarde, tijd_s):
        self.waarde, self.tijd_s = waarde, tijd_s
    def lees(self):
        return Meting(self.waarde, self.tijd_s)

class Regelaar:
    def __init__(self, sensor, kp=200.0, grens=2000.0, max_leeftijd=5.0):
        if (not all(isfinite(x) for x in (kp, grens, max_leeftijd))
                or kp < 0 or grens <= 0 or max_leeftijd <= 0):
            raise ValueError("ongeldige instelling")
        self.sensor = sensor             # samenstelling, geen Sensor-subklasse
        self.kp, self.grens = kp, grens
        self.max_leeftijd = max_leeftijd
    def stap(self, setpoint, nu):
        if not isfinite(setpoint) or not -30 <= setpoint <= 60 or not isfinite(nu):
            raise ValueError("ongeldig setpoint of tijdstip")
        m = self.sensor.lees()
        if (m.eenheid != "degC" or not isfinite(m.waarde) or not isfinite(m.tijd_s)
                or not -30 <= m.waarde <= 60
                or not 0 <= nu - m.tijd_s <= self.max_leeftijd):
            raise ValueError("meting onbruikbaar; supervisor kiest veilige modus")
        return min(self.grens, max(0.0, self.kp * (setpoint - m.waarde)))


class GekalibreerdeProefSensor(ProefSensor):
    def __init__(self, waarde, tijd_s, offset):
        super().__init__(waarde, tijd_s)
        if not isfinite(offset): raise ValueError("offset niet eindig")
        self.offset = offset
    def lees(self):
        m = super().lees()
        return Meting(m.waarde+self.offset, m.tijd_s, m.eenheid)

ruw = ProefSensor(18.3,10)
geijkt = GekalibreerdeProefSensor(18.3,10,-0.3)
for sensor in (ruw, geijkt):
    print(type(sensor).__name__, Regelaar(sensor).stap(20,11), "W")
assert abs(Regelaar(geijkt).stap(20,11)-400)<1e-10
