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 13, 2023
1 parent 44ab8aa commit c823ff8
Show file tree
Hide file tree
Showing 9 changed files with 543 additions and 6 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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,57 @@ Every other values assigned to `W1THERMSENSOR_NO_KERNEL_MODULE` will case `w1the

*Note: the examples above also apply for the CLI tool usage. See below.*

### Correcting Temperatures / Sensor Calibration
Calibrating the temperature sensor relies on obtaining a measured high and measured low value that
have known reference values that can be used for correcting the sensor's readings. The simplest
way to do this is to measure the melting point and boiling point of water since those values are
known. This method will only work with waterproof sensors - you will need a different mechanism
for obtaining measured values if you are not using a waterproof sensor.

In order to obtain the `measured_low_point`, fill a container to 80% with ice and add water to the
ice until the ice is floating and water is at the surface. Submerse your sensor in the ice water,
ensuring it does not touch the container. Wait 5 minutes for the temperature to stabilize in the
container and then once the sensor readings have stabilized for approximately 30 seconds (readings
remain consistent), record the value as the `measured_low_point`

In order to obtain the `measured_high_point`, bring a pot of water to a rapid boil. Place your
sensor in the boiling water, ensuring that it does not touch the pot. Allow the sensor to come up
to temperature and once it has stabilized for approximately 30 seconds (readings remain
consistent), record the value as the `measured_high_point`

Generally speaking, the `reference_low_point` should be left at 0.0 unless you have some special
situation that changes the melting point of water. Because melting does not involve a gaseous
phase change, the effects of air pressure and altitude on the melting point are minimal.

The `reference_high_point` on the other hand is greatly impacted by air pressure (and thus
altitude). For example, the boiling point of water is 100.0C at sea level, and is approximately
72C at the summit of Mount Everest (8848m above sea level). While air pressure is what actually
dictates boiling point, generally speaking altitude is a close enough approximation for most use
cases. [Engineering Toolbox](https://www.engineeringtoolbox.com/boiling-points-water-altitude-d_1344.html)
has a page that gives you the boiling point of water at different altitudes.

This method is derived from [this Instructable](https://www.instructables.com/Calibration-of-DS18B20-Sensor-With-Arduino-UNO/).

```python
from w1thermsensor.calibration_data import CalibrationData
from w1thermsensor import W1ThermSensor, Unit

calibration_data = CalibrationData(
measured_high_point=measured_high_point,
measured_low_point=measured_low_point,
reference_high_point=reference_high_point,
reference_low_point=reference_low_point, # optional, defaults to 0.0
)
sensor = W1ThermSensor(calibration_data=calibration_data)

corrected_temperature_in_celsius = sensor.get_corrected_temperature()
corrected_temperature_in_fahrenheit = sensor.get_corrected_temperature(Unit.DEGREES_F)
corrected_temperature_in_all_units = sensor.get_corrected_temperatures([
Unit.DEGREES_C,
Unit.DEGREES_F,
Unit.KELVIN])
```

### Async Interface

The `w1thermsensor` package implements an async interface `AsyncW1ThermSensor` for asyncio.
Expand Down
58 changes: 56 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: # type: ignore
"""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,28 @@ async def get_temperatures(self, units: Iterable[Unit]) -> List[float]: # type:
for unit in units
]

async def get_corrected_temperatures(self, # type: ignore
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
measured_low_point: float
reference_high_point: float
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)
59 changes: 56 additions & 3 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 Expand Up @@ -462,7 +515,7 @@ def convert_raw_temperature_to_sensor_count(raw_temperature_line: str) -> int:
if int16 >> 15 == 0:
return int16 # positive values need no processing
else:
# substract 2^16 to get correct negative value
# subtract 2^16 to get correct negative value
return int16 - (1 << 16)


Expand Down
Loading

0 comments on commit c823ff8

Please sign in to comment.