added POC for temperature logging

This commit is contained in:
markusro committed 2026-07-11 22:11:03 +02:00
1 parent d48f96e498
commit 5338304ee8
6 files changed
+768 -7

No files matched your search

+39 -1
View File
@@ -3,6 +3,8 @@ import re
import operator
from functools import reduce
from .temperature import TemperatureController, register_controller
DEBUG=False
@@ -40,7 +42,7 @@ def checksum(message):
bcc = (reduce(operator.xor, list(map(ord,message))))
return chr(bcc)
class Eurotherm(object):
class Eurotherm(TemperatureController):
def __init__(self, serial_device, baudrate = 19200):
self.device = standard_device
# timeout: 110 ms to get all answers.
@@ -100,6 +102,39 @@ class Eurotherm(object):
def get_setpoint_temperature(self):
return self.read_param('SL')
# TemperatureController interface implementation
def get_temperature(self) -> float:
"""Read current temperature in Celsius."""
temp = self.read_param('PV')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def get_setpoint(self) -> float:
"""Read target setpoint temperature in Celsius."""
temp = self.read_param('SL')
if temp is None:
return 0.0
try:
return float(temp)
except (ValueError, TypeError):
return 0.0
def set_setpoint(self, temperature: float) -> bool:
"""Set target temperature in Celsius."""
return self.write_param('SL', str(temperature))
def close(self):
"""Close the serial connection."""
if hasattr(self, 's') and self.s is not None:
if self.s.is_open:
self.s.close()
self.s = None
if __name__ == '__main__':
import time
delta=5
@@ -118,3 +153,6 @@ if __name__ == '__main__':
f.write(l)
f.flush()
f.write('# MARK -- %s --\n'%(time.asctime()))
# Register Eurotherm with the factory
register_controller('eurotherm', Eurotherm)
+143
View File
@@ -0,0 +1,143 @@
# -*- coding: iso-8859-1 -*-
"""
Temperature controller interface and base classes.
This module provides a consistent interface for temperature controllers
to enable pluggable temperature monitoring systems.
"""
from abc import ABC, abstractmethod
# Plugin registry for temperature controllers
_controller_registry = {}
def register_controller(name: str, controller_class):
"""
Register a temperature controller implementation.
Args:
name: Unique identifier for this controller type
controller_class: Class that implements TemperatureController
"""
_controller_registry[name] = controller_class
def create_controller(name: str, *args, **kwargs):
"""
Create a temperature controller instance by name.
Args:
name: Registered controller type name
*args: Positional arguments to pass to controller constructor
**kwargs: Keyword arguments to pass to controller constructor
Returns:
TemperatureController: Instance of the requested controller
Raises:
ValueError: If controller name is not registered and cannot be imported
"""
# If not found, try to auto-import the module
if name not in _controller_registry:
# Map known controller names to their modules
module_map = {
'simulated': 'damaris.tools.temperature_simulator',
'eurotherm': 'damaris.tools.eurotherm',
}
if name in module_map:
try:
# Import the module which will register the controller
import importlib
importlib.import_module(module_map[name])
except ImportError as e:
raise ValueError(f"Failed to import controller '{name}': {e}. Available: {list(_controller_registry.keys())}")
# Check again after import
if name not in _controller_registry:
raise ValueError(f"Unknown temperature controller: {name}. Available: {list(_controller_registry.keys())}")
return _controller_registry[name](*args, **kwargs)
def get_controller_names() -> list:
"""
Get list of available controller names.
Returns:
list: List of registered controller type names
"""
return list(_controller_registry.keys())
# Pre-register built-in controllers by importing their modules
# This ensures they're available without explicit imports
try:
from . import eurotherm
except ImportError as e:
# Eurotherm may require serial module which might not be installed
# That's OK, it will be imported on-demand if needed
pass
try:
from . import temperature_simulator
except ImportError as e:
# Simulator should always work (no external dependencies)
# But if it fails, it will be imported on-demand
pass
class TemperatureController(ABC):
"""
Abstract base class for temperature controllers.
All temperature controller implementations (Eurotherm, Lakeshore, etc.)
should inherit from this class and implement the abstract methods.
"""
@abstractmethod
def get_temperature(self) -> float:
"""
Read current temperature in Celsius.
Returns:
float: Current temperature in degrees Celsius.
Raises:
IOError: If communication with the device fails.
RuntimeError: If the device is not ready or returns invalid data.
"""
pass
@abstractmethod
def get_setpoint(self) -> float:
"""
Read the target setpoint temperature in Celsius.
Returns:
float: Target temperature in degrees Celsius.
"""
pass
@abstractmethod
def set_setpoint(self, temperature: float) -> bool:
"""
Set the target temperature setpoint.
Args:
temperature: Target temperature in degrees Celsius.
Returns:
bool: True if the setpoint was successfully set, False otherwise.
"""
pass
def close(self):
"""
Clean up resources (e.g., close serial connections).
This method should be called when the controller is no longer needed.
The default implementation does nothing; subclasses should override
if they have resources to clean up.
"""
pass
+126
View File
@@ -0,0 +1,126 @@
# -*- coding: iso-8859-1 -*-
"""
Simulated temperature controller for testing.
Provides a temperature controller that simulates real temperature readings
with configurable parameters including sinusoidal variation, noise, and missing values.
"""
import time
import math
import random
import threading
from .temperature import TemperatureController, register_controller
class SimulatedTemperatureController(TemperatureController):
"""
A simulated temperature controller that generates realistic temperature readings.
This controller is useful for testing and development without requiring
physical hardware. It simulates temperature readings with:
- Base temperature around 290K (17C)
- Sinusoidal variation with amplitude of 10K
- 5% random noise
- 10% probability of missing values (returns previous value)
- 60 second period for the sine wave
Attributes:
base_temp: Base temperature in Kelvin (default: 290.0)
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
period: Period of sine wave in seconds (default: 60.0)
noise_level: Fraction of random noise (default: 0.05 = 5%)
missing_probability: Probability of missing value (default: 0.10 = 10%)
"""
def __init__(self, base_temp: float = 290.0, amplitude: float = 10.0,
period: float = 60.0, noise_level: float = 0.05,
missing_probability: float = 0.10):
"""
Initialize the simulated temperature controller.
Args:
base_temp: Base temperature in Kelvin (default: 290.0)
amplitude: Amplitude of sinusoidal variation in Kelvin (default: 10.0)
period: Period of sine wave in seconds (default: 60.0)
noise_level: Fraction of random noise (default: 0.05 = 5%)
missing_probability: Probability of missing value (default: 0.10 = 10%)
"""
self.base_temp = float(base_temp)
self.amplitude = float(amplitude)
self.period = float(period)
self.noise_level = float(noise_level)
self.missing_probability = float(missing_probability)
self._start_time = time.time()
self._lock = threading.RLock()
self._setpoint = self.base_temp
self._last_temp = self.base_temp
self._last_reading_time = self._start_time
def get_temperature(self) -> float:
"""
Get simulated current temperature in Kelvin.
Returns:
float: Simulated temperature in Kelvin
"""
with self._lock:
elapsed = time.time() - self._start_time
current_time = time.time()
# Calculate ideal sinusoidal temperature
temp = self.base_temp + self.amplitude * math.sin(2 * math.pi * elapsed / self.period)
# Add 5% random noise
noise = temp * self.noise_level * (random.random() * 2 - 1) # -5% to +5%
temp += noise
# 10% chance of missing value (return last good value)
if random.random() < self.missing_probability:
temp = self._last_temp
else:
self._last_temp = temp
self._last_reading_time = current_time
return temp
def get_setpoint(self) -> float:
"""
Get the target setpoint temperature in Kelvin.
Returns:
float: Target temperature in Kelvin
"""
with self._lock:
return self._setpoint
def set_setpoint(self, temperature: float) -> bool:
"""
Set the target temperature setpoint.
Args:
temperature: Target temperature in Kelvin
Returns:
bool: True if successful, False otherwise
"""
with self._lock:
self._setpoint = float(temperature)
return True
def close(self):
"""Clean up resources."""
pass
def reset(self):
"""Reset the simulation to initial state."""
with self._lock:
self._start_time = time.time()
self._last_temp = self.base_temp
self._last_reading_time = self._start_time
# Register the simulated controller with the factory
register_controller('simulated', SimulatedTemperatureController)