Skip to content

Commit

Permalink
feature: Add ability to provide sensor calibration data and get corre…
Browse files Browse the repository at this point in the history
…cted temperatures.
  • Loading branch information
brettlounsbury committed Sep 11, 2023
1 parent 44ab8aa commit d7bbdb9
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# emacs backup files
*~

# pycharm files
.idea/

# python bytecode files
*.pyc

Expand All @@ -17,6 +20,7 @@ w1thermsensor.egg-info

# coverage data
.coverage*
coverage.xml

# ignore temporary tox environments
.tox
Expand Down
57 changes: 55 additions & 2 deletions src/w1thermsensor/async_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
from typing import Iterable, List

from w1thermsensor.core import W1ThermSensor, evaluate_resolution, evaluate_temperature
from w1thermsensor.errors import NoSensorFoundError, SensorNotReadyError, W1ThermSensorError
from w1thermsensor.errors import (
InvalidCalibrationDataError,
NoSensorFoundError,
SensorNotReadyError,
W1ThermSensorError
)
from w1thermsensor.units import Unit


Expand Down Expand Up @@ -81,7 +86,7 @@ async def get_temperature(self, unit: Unit = Unit.DEGREES_C) -> float: # type:
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises ResetValueError: if the sensor has still the initial value and no measurment
:raises ResetValueError: if the sensor has still the initial value and no measurement
"""
raw_temperature_line = (await self.get_raw_sensor_strings())[1]
return evaluate_temperature(
Expand All @@ -94,6 +99,33 @@ async def get_temperature(self, unit: Unit = Unit.DEGREES_C) -> float: # type:
self.SENSOR_RESET_VALUE,
)

async def get_corrected_temperature(self, unit: Unit = Unit.DEGREES_C) -> float:
"""Returns the temperature in the specified unit, corrected based on the calibration data
:param int unit: the unit of the temperature requested
:returns: the temperature in the given unit
:rtype: float
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises ResetValueError: if the sensor has still the initial value and no measurement
:raises InvalidCalibrationDataError: if the calibration data was not provided at creation
"""

if not self.calibration_data:
raise InvalidCalibrationDataError(
"calibration_data must be provided to provide corrected temperature readings",
None,
)

raw_temperature = await self.get_temperature(Unit.DEGREES_C)
corrected_temperature = self.calibration_data.correct_temperature_for_calibration_data(
raw_temperature)

return Unit.get_conversion_function(Unit.DEGREES_C, unit)(corrected_temperature)

async def get_temperatures(self, units: Iterable[Unit]) -> List[float]: # type: ignore
"""Returns the temperatures in the specified units
Expand All @@ -113,6 +145,27 @@ async def get_temperatures(self, units: Iterable[Unit]) -> List[float]: # type:
for unit in units
]

async def get_corrected_temperatures(self, units: Iterable[Unit]) -> List[float]:
"""Returns the temperatures in the specified units, corrected based on the calibration data
:param list units: the units for the sensor temperature
:returns: the sensor temperature in the given units. The order of
the temperatures matches the order of the given units.
:rtype: list
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises InvalidCalibrationDataError: if the calibration data was not provided at creation
"""

corrected_temperature = await self.get_corrected_temperature(Unit.DEGREES_C)
return [
Unit.get_conversion_function(Unit.DEGREES_C, unit)(corrected_temperature)
for unit in units
]

async def get_resolution(self) -> int: # type: ignore
"""Get the current resolution from the sensor.
Expand Down
112 changes: 112 additions & 0 deletions src/w1thermsensor/calibration_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
w1thermsensor
~~~~~~~~~~~~~
A Python package and CLI tool to work with w1 temperature sensors.
:copyright: (c) 2023 by Brett Lounsbury
:license: MIT, see LICENSE for more details.
"""

from dataclasses import dataclass

from w1thermsensor.errors import InvalidCalibrationDataError


@dataclass(frozen=True)
class CalibrationData:
"""
This Class represents the data required for calibrating a temperature sensor and houses the
logic to correct the temperature sensor's raw readings based on the calibration data.
The method used for this class requires that you collect temperature readings of the low point
and high point of water in your location with your sensor (this method obviously requires a
waterproof sensor).
To gather the low point: Take a large cup and fill it completely with ice and then add water.
Submerse your temperature sensor ensuring it does not touch the cup. Wait 2 minutes, and then
begin polling the temperature sensor. Once the temperature stabilizes and stays around the
same value for 30 seconds, record this as your measured_low_point.
To gather the high point: Bring a pot of water to a rapid boil. Submerse your temperature
sensor ensuring it
does not touch the pot. Begin polling the sensor. Once the temperature stabilizes and
stays around the same
value for 30 seconds, record this as your measured_high_point.
To gather the reference high point: The high point changes significantly with air pressure
(and therefore altitude). The easiest way to get the reference data for the high point is to
find out the elevation of your location and then find the high point at that elevation.
This is not perfectly accurate, but it is generally close enough for most use cases.
You can find this data here:
https://www.engineeringtoolbox.com/high-points-water-altitude-d_1344.html
To gather the reference low point: The low point of water does not change significantly with
altitude like the high point does because it does not involve a gas (water vapor) and therefore
air pressure is not as big of a factor. Generally speaking the default value of 0.0 is
accurate enough.
You MUST provide the measured_low_point, measured_high_point, and reference_high_point in
Celsius.
This class is based on:
https://www.instructables.com/Calibration-of-DS18B20-Sensor-With-Arduino-UNO/
"""

measured_high_point: float = None
measured_low_point: float = None
reference_high_point: float = None
reference_low_point: float = 0.0

def __post_init__(self):
"""
Validates that the required arguments are set and sanity check that the high points are
higher than the associated low points. This method does not sanity check that values make
sense for high/low point outside of high_point > low_point.
"""

if self.measured_high_point is None:
raise InvalidCalibrationDataError(
"Measured high point must be provided.", self.__str__()
)

if self.measured_low_point is None:
raise InvalidCalibrationDataError(
"Measured low point must be provided.", self.__str__()
)

if self.reference_high_point is None:
raise InvalidCalibrationDataError(
"Reference high point must be provided.", self.__str__()
)

if self.reference_low_point is None:
raise InvalidCalibrationDataError(
"Reference low point must not set to None.", self.__str__()
)

if self.measured_low_point >= self.measured_high_point:
raise InvalidCalibrationDataError(
"Measured low point must be less than measured high point. Did you reverse the " +
"values?",
self.__str__(),
)

if self.reference_low_point >= self.reference_high_point:
raise InvalidCalibrationDataError(
"Reference low point must be less than reference high point. Did you reverse " +
"the values?",
self.__str__(),
)

def correct_temperature_for_calibration_data(self, raw_temperature):
"""
Correct the temperature based on the calibration data provided. This is done by taking
the raw temperature reading and subtracting out the measured low point, scaling that by the
scaling factor, and then adding back the reference low point.
"""
reference_range = self.reference_high_point - self.reference_low_point
measured_range = self.measured_high_point - self.measured_low_point
scaling_factor = reference_range / measured_range
return ((raw_temperature - self.measured_low_point) * scaling_factor
+ self.reference_low_point)
57 changes: 55 additions & 2 deletions src/w1thermsensor/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
from pathlib import Path
from typing import Iterable, List, Optional, Union

from w1thermsensor.calibration_data import CalibrationData
from w1thermsensor.errors import (
InvalidCalibrationDataError,
NoSensorFoundError,
ResetValueError,
SensorNotReadyError,
W1ThermSensorError,
UnsupportedSensorError,
W1ThermSensorError
)
from w1thermsensor.sensors import Sensor
from w1thermsensor.units import Unit
Expand Down Expand Up @@ -118,6 +120,7 @@ def __init__(
sensor_id: Optional[str] = None,
offset: float = 0.0,
offset_unit: Unit = Unit.DEGREES_C,
calibration_data: Optional[CalibrationData] = None,
) -> None:
"""Initializes a W1ThermSensor.
Expand Down Expand Up @@ -153,6 +156,8 @@ def __init__(
self.id) / self.SLAVE_FILE
)

self.calibration_data = calibration_data

if not self.exists():
raise NoSensorFoundError(
"Could not find sensor of type {} with id {}".format(
Expand Down Expand Up @@ -267,7 +272,7 @@ def get_temperature(self, unit: Unit = Unit.DEGREES_C) -> float:
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises ResetValueError: if the sensor has still the initial value and no measurment
:raises ResetValueError: if the sensor has still the initial value and no measurement
"""
raw_temperature_line = self.get_raw_sensor_strings()[1]
return evaluate_temperature(
Expand All @@ -280,6 +285,33 @@ def get_temperature(self, unit: Unit = Unit.DEGREES_C) -> float:
self.SENSOR_RESET_VALUE,
)

def get_corrected_temperature(self, unit: Unit = Unit.DEGREES_C) -> float:
"""Returns the temperature in the specified unit, corrected based on the calibration data
:param int unit: the unit of the temperature requested
:returns: the temperature in the given unit
:rtype: float
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises ResetValueError: if the sensor has still the initial value and no measurement
:raises InvalidCalibrationDataError: if the calibration data was not provided at creation
"""

if not self.calibration_data:
raise InvalidCalibrationDataError(
"calibration_data must be provided to provide corrected temperature readings",
None,
)

raw_temperature = self.get_temperature(Unit.DEGREES_C)
corrected_temperature = self.calibration_data.correct_temperature_for_calibration_data(
raw_temperature)

return Unit.get_conversion_function(Unit.DEGREES_C, unit)(corrected_temperature)

def get_temperatures(self, units: Iterable[Unit]) -> List[float]:
"""Returns the temperatures in the specified units
Expand All @@ -299,6 +331,27 @@ def get_temperatures(self, units: Iterable[Unit]) -> List[float]:
for unit in units
]

def get_corrected_temperatures(self, units: Iterable[Unit]) -> List[float]:
"""Returns the temperatures in the specified units, corrected based on the calibration data
:param list units: the units for the sensor temperature
:returns: the sensor temperature in the given units. The order of
the temperatures matches the order of the given units.
:rtype: list
:raises UnsupportedUnitError: if the unit is not supported
:raises NoSensorFoundError: if the sensor could not be found
:raises SensorNotReadyError: if the sensor is not ready yet
:raises InvalidCalibrationDataError: if the calibration data was not provided at creation
"""

corrected_temperature = self.get_corrected_temperature(Unit.DEGREES_C)
return [
Unit.get_conversion_function(Unit.DEGREES_C, unit)(corrected_temperature)
for unit in units
]

def get_resolution(self) -> int:
"""Get the current resolution from the sensor.
Expand Down
9 changes: 9 additions & 0 deletions src/w1thermsensor/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,12 @@ def __init__(self, sensor_id):
"Sensor {} yields the reset value of 85 degree millicelsius. "
"Please check the power-supply for the sensor.".format(sensor_id)
)


class InvalidCalibrationDataError(W1ThermSensorError):
"""Exception when the calibration data provided is invalid"""

def __init__(self, message, calibration_data):
super().__init__(
"Calibration data {} is invalid: {}.".format(message, calibration_data)
)
Loading

0 comments on commit d7bbdb9

Please sign in to comment.