diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 4f03581..d990a98 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -8,6 +8,4 @@ on: jobs: build_and_deploy: uses: sensirion/.github/.github/workflows/driver.python.pypi_publish.yml@main - secrets: - PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - + secrets: inherit diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index d200c56..d2d0f78 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -2,9 +2,11 @@ name: Publish Documentation on: workflow_dispatch: push: - tags: - - "[0-9]+.[0-9]+.[0-9]+" + branches: + - main jobs: documentation: uses: sensirion/.github/.github/workflows/driver.python.documentation.yml@main + with: + use-project-requirements: true diff --git a/.github/workflows/gh_workflow_metadata_update.yml b/.github/workflows/gh_workflow_metadata_update.yml new file mode 100644 index 0000000..7dd70ed --- /dev/null +++ b/.github/workflows/gh_workflow_metadata_update.yml @@ -0,0 +1,11 @@ +name: Driver Index Metadata Update + +on: + push: + branches: + - main + +jobs: + driver-index-metadata-update: + uses: sensirion/.github/.github/workflows/driver.common.di_metadata_update.yml@main + secrets: inherit diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c12476..ba079ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.3.0] - 2026-6-5 + +### Added + +- Added get_product_type command. ## [1.2.0] - 2025-3-13 ### Added @@ -40,7 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add interfaces to start, stop and read measurements. - Add interfaces to read product name, serial number and version -[Unreleased]: https://github.com/Sensirion/python-i2c-sen66/compare/1.2.0...HEAD +[Unreleased]: https://github.com/Sensirion/python-i2c-sen66/compare/1.3.0...HEAD +[1.3.0]: https://github.com/Sensirion/python-i2c-sen66/compare/1.2.0...1.3.0 [1.2.0]: https://github.com/Sensirion/python-i2c-sen66/compare/1.1.0...1.2.0 [1.1.0]: https://github.com/Sensirion/python-i2c-sen66/compare/1.0.1...1.1.0 [1.0.1]: https://github.com/Sensirion/python-i2c-sen66/compare/1.0.0...1.0.1 diff --git a/LICENSE b/LICENSE index 2062766..8fe04ed 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2025, Sensirion AG +Copyright (c) 2026, Sensirion AG All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index ae89917..c826930 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This repository contains the Python driver to communicate with a Sensirion SEN66 sensor over I2C. -SEN66 picture @@ -10,18 +10,18 @@ Click [here](https://sensirion.com/sen6x-air-quality-sensor-platform) to learn m -The default I²C address of [SEN66](https://www.sensirion.com/products/catalog/SEN66) is **0x6B**. +The default I²C address of [SEN66](https://www.sensirion.com/products/catalog/SEN66) is **0x6b**. ## Connect the sensor -You can connect your sensor over a [SEK-SensorBridge](https://developer.sensirion.com/sensirion-products/sek-sensorbridge/). +You can connect your sensor over a [SEK-SensorBridge](https://developer.sensirion.com/product-support/sek-sensorbridge/). For special setups you find the sensor pinout in the section below.
Sensor pinout

-sensor wiring picture | *Pin* | *Cable Color* | *Name* | *Description* | *Comments* | diff --git a/ci/checkin_doc.sh b/ci/checkin_doc.sh deleted file mode 100644 index 1031751..0000000 --- a/ci/checkin_doc.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash - -# set shell settings (see https://sipb.mit.edu/doc/safe-shell/) -set -euv -o pipefail - -# clone the repo into a subfolder html, checkout the gh-pages into this folder and commit the freshly generated html -git clone "git@gitlab:${CI_PROJECT_PATH}.git" html -cd html -git checkout gh-pages -rm -f empty.txt -rm -f *.html -rm -f *.js -rm -rf _* -cd .. -# make sure to copy .nojekyll -cp -rf public/.[!.]* public/* html -cd html -git add . - -if git diff-index --quiet HEAD -- -then - exit 0 -fi - -git commit -m"Automatic doc update: ${CI_COMMIT_SHORT_SHA}" -git push \ No newline at end of file diff --git a/ci/set_git_config.sh b/ci/set_git_config.sh deleted file mode 100644 index 860c6aa..0000000 --- a/ci/set_git_config.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# set shell settings (see https://sipb.mit.edu/doc/safe-shell/) -set -eufv -o pipefail - -# install ssh private key (set as CI variable in GitLab project settings) -mkdir -p ~/.ssh -echo "$SSH_DEPLOY_KEY" > ~/.ssh/id_rsa -chmod 400 ~/.ssh/id_rsa - -# change remote URL to SSH to allow pushing with SSH -git remote set-url --push origin "git@gitlab:${CI_PROJECT_PATH}.git" - -# set git author -git config --global user.name "GitLab-CI" -git config --global user.email "<>" diff --git a/docs/conf.py b/docs/conf.py index 4cbbb71..b7326c4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -4,11 +4,12 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html + import os import sys from datetime import datetime -import pkg_resources +import importlib.metadata as metadata import sphinx.ext.autodoc import sensirion_i2c_sen66 @@ -17,16 +18,16 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) # -- Project information ----------------------------------------------------- -distribution = pkg_resources.get_distribution('sensirion_i2c_sen66') +distribution = metadata.distribution("sensirion_i2c_sen66") -project = u'sensirion_i2c_sen66' +project = distribution.name copyright = u'{} Sensirion AG, Switzerland'.format(datetime.now().year) author = 'Sensirion AG' # The short X.Y version -version = sensirion_i2c_sen66.__version__ +version = distribution.version # The full version, including alpha/beta/rc tags -release = sensirion_i2c_sen66.__version__ +release = distribution.version # -- General configuration --------------------------------------------------- diff --git a/docs/execute-measurements.rst b/docs/execute-measurements.rst index 36f0fe7..3028026 100644 --- a/docs/execute-measurements.rst +++ b/docs/execute-measurements.rst @@ -26,7 +26,7 @@ execute a simple measurement. python examples/example_usage_sensorbridge_sen66.py --serial-port -.. _Sensirion SEK-SensorBridge: https://developer.sensirion.com/sensirion-products/sek-sensorbridge/ +.. _Sensirion SEK-SensorBridge: https://developer.sensirion.com/product-support/sek-sensorbridge/ .. _SensorBridge FTDI Driver Installation: https://sensirion.github.io/python-shdlc-sensorbridge/sensor-bridge-installation.html Example script diff --git a/examples/SEN6x_I2C_FRC_CO2_example_linux_sen66.py b/examples/SEN6x_I2C_FRC_CO2_example_linux_sen66.py new file mode 100644 index 0000000..85045bf --- /dev/null +++ b/examples/SEN6x_I2C_FRC_CO2_example_linux_sen66.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Wait at least one second after power on + time.sleep(1.0) + + # Perform forced recalibration + correction = sensor.perform_forced_co2_recalibration(600) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_FRC_CO2_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_FRC_CO2_example_sensorbridge_sen66.py new file mode 100644 index 0000000..b70b297 --- /dev/null +++ b/examples/SEN6x_I2C_FRC_CO2_example_sensorbridge_sen66.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Wait at least one second after power on + time.sleep(1.0) + + # Perform forced recalibration + correction = sensor.perform_forced_co2_recalibration(600) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_change_NOX_parameters_example_linux_sen66.py b/examples/SEN6x_I2C_change_NOX_parameters_example_linux_sen66.py new file mode 100644 index 0000000..c25b778 --- /dev/null +++ b/examples/SEN6x_I2C_change_NOX_parameters_example_linux_sen66.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set nox parameters: + # index_offset = 1 + # learning_time_offset_hours = 12 + # learning_time_gain_hours = 12 + # gating_max_duration_minutes = 720 + # std_initial = 50 + # gain_factor = 230 + sensor.set_nox_algorithm_tuning_parameters(1, 12, 12, 720, 50, 230) + + # Start continuous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_change_NOX_parameters_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_change_NOX_parameters_example_sensorbridge_sen66.py new file mode 100644 index 0000000..35defd8 --- /dev/null +++ b/examples/SEN6x_I2C_change_NOX_parameters_example_sensorbridge_sen66.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set nox parameters: + # index_offset = 1 + # learning_time_offset_hours = 12 + # learning_time_gain_hours = 12 + # gating_max_duration_minutes = 720 + # std_initial = 50 + # gain_factor = 230 + sensor.set_nox_algorithm_tuning_parameters(1, 12, 12, 720, 50, 230) + + # Start continuous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_change_VOC_parameters_example_linux_sen66.py b/examples/SEN6x_I2C_change_VOC_parameters_example_linux_sen66.py new file mode 100644 index 0000000..668ca57 --- /dev/null +++ b/examples/SEN6x_I2C_change_VOC_parameters_example_linux_sen66.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set voc paramters: + # index_offset = 100 + # learning_time_offset_hours = 12 + # learning_time_gain_hours = 12 + # gating_max_duration_minutes = 180 + # std_initial = 50 + # gain_factor = 230 + sensor.set_voc_algorithm_tuning_parameters(100, 12, 12, 180, 50, 230) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_change_VOC_parameters_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_change_VOC_parameters_example_sensorbridge_sen66.py new file mode 100644 index 0000000..1e4f11f --- /dev/null +++ b/examples/SEN6x_I2C_change_VOC_parameters_example_sensorbridge_sen66.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set voc paramters: + # index_offset = 100 + # learning_time_offset_hours = 12 + # learning_time_gain_hours = 12 + # gating_max_duration_minutes = 180 + # std_initial = 50 + # gain_factor = 230 + sensor.set_voc_algorithm_tuning_parameters(100, 12, 12, 180, 50, 230) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_config_STAR_example_linux_sen66.py b/examples/SEN6x_I2C_config_STAR_example_linux_sen66.py new file mode 100644 index 0000000..107bce0 --- /dev/null +++ b/examples/SEN6x_I2C_config_STAR_example_linux_sen66.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set the temperature acceleration parameters + # k = 1 + # p = 1 + # t1 = 1 + # t2 = 1 + sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_config_STAR_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_config_STAR_example_sensorbridge_sen66.py new file mode 100644 index 0000000..ae978f0 --- /dev/null +++ b/examples/SEN6x_I2C_config_STAR_example_sensorbridge_sen66.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set the temperature acceleration parameters + # k = 1 + # p = 1 + # t1 = 1 + # t2 = 1 + sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) + + # Start continous measurement + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_config_coldstart_example_linux_sen66.py b/examples/SEN6x_I2C_config_coldstart_example_linux_sen66.py new file mode 100644 index 0000000..0ba9d8a --- /dev/null +++ b/examples/SEN6x_I2C_config_coldstart_example_linux_sen66.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set the temperature offset parameters: + # offset = -400 + # slope = 0.02 * 1000 + # time_constant = 800 + # slot = 0 + sensor.set_temperature_offset_parameters(-400, 20, 800, 0) + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_config_coldstart_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_config_coldstart_example_sensorbridge_sen66.py new file mode 100644 index 0000000..f1a1784 --- /dev/null +++ b/examples/SEN6x_I2C_config_coldstart_example_sensorbridge_sen66.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + # Set the temperature offset parameters: + # offset = -400 + # slope = 0.02 * 1000 + # time_constant = 800 + # slot = 0 + sensor.set_temperature_offset_parameters(-400, 20, 800, 0) + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_heater_example_linux_sen66.py b/examples/SEN6x_I2C_heater_example_linux_sen66.py new file mode 100644 index 0000000..27facc3 --- /dev/null +++ b/examples/SEN6x_I2C_heater_example_linux_sen66.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.activate_sht_heater() + humi = 0 + while True: + time.sleep(0.05) + (humi, t + ) = sensor.get_sht_heater_measurements() + print(f"current temperature: {t}" + ) + print(f"current humidity: {humi}" + ) + if humi == 32767: + break + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_heater_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_heater_example_sensorbridge_sen66.py new file mode 100644 index 0000000..9d9ccbe --- /dev/null +++ b/examples/SEN6x_I2C_heater_example_sensorbridge_sen66.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.activate_sht_heater() + humi = 0 + while True: + time.sleep(0.05) + (humi, t + ) = sensor.get_sht_heater_measurements() + print(f"current temperature: {t}" + ) + print(f"current humidity: {humi}" + ) + if humi == 32767: + break + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_minimal_example_linux_sen66.py b/examples/SEN6x_I2C_minimal_example_linux_sen66.py new file mode 100644 index 0000000..35fc3e0 --- /dev/null +++ b/examples/SEN6x_I2C_minimal_example_linux_sen66.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_minimal_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_minimal_example_sensorbridge_sen66.py new file mode 100644 index 0000000..2d490e5 --- /dev/null +++ b/examples/SEN6x_I2C_minimal_example_sensorbridge_sen66.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout data from the sensor + (mass_concentration_pm1p0, mass_concentration_pm2p5, mass_concentration_pm4p0, mass_concentration_pm10p0, + ambient_humidity, ambient_temperature, voc_index, nox_index, co2 + ) = sensor.read_measured_values_as_integers() + print(f"Mass concentration pm1p0 [µg/m³]: {mass_concentration_pm1p0 / 10.0}" + ) + print(f"Mass concentration pm2p5 [µg/m³]: {mass_concentration_pm2p5 / 10.0}" + ) + print(f"Mass concentration pm4p0 [µg/m³]: {mass_concentration_pm4p0 / 10.0}" + ) + print(f"Mass concentration pm10p0 [µg/m³]: {mass_concentration_pm10p0 / 10.0}" + ) + print(f"Ambient humidity [%]: {ambient_humidity / 100.0}" + ) + print(f"Ambient temperature [°C]: {ambient_temperature / 200.0}" + ) + print(f"VOC index: {voc_index}" + ) + print(f"NOX index: {nox_index}" + ) + print(f"CO₂ [ppm]: {co2}" + ) diff --git a/examples/SEN6x_I2C_read_raw_example_linux_sen66.py b/examples/SEN6x_I2C_read_raw_example_linux_sen66.py new file mode 100644 index 0000000..2f49269 --- /dev/null +++ b/examples/SEN6x_I2C_read_raw_example_linux_sen66.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import LinuxI2cTransceiver, I2cConnection, CrcCalculator +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--i2c-port', '-p', default='/dev/i2c-1') +args = parser.parse_args() + +with LinuxI2cTransceiver(args.i2c_port) as i2c_transceiver: + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout raw data from the sensor + (raw_humidity, raw_temperature, raw_voc, raw_nox, raw_co2 + ) = sensor.read_measured_raw_values() + print(f"Raw humidity: {raw_humidity}" + ) + print(f"Raw temperature: {raw_temperature}" + ) + print(f"Raw VOC index: {raw_voc}" + ) + print(f"Raw NOX index: {raw_nox}" + ) + print(f"Raw CO₂: {raw_co2}" + ) diff --git a/examples/SEN6x_I2C_read_raw_example_sensorbridge_sen66.py b/examples/SEN6x_I2C_read_raw_example_sensorbridge_sen66.py new file mode 100644 index 0000000..4dda647 --- /dev/null +++ b/examples/SEN6x_I2C_read_raw_example_sensorbridge_sen66.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) Copyright 2026 Sensirion AG, Switzerland +# +# THIS FILE IS AUTOMATICALLY GENERATED! +# +# Generator: sensirion-driver-generator 1.7.0 +# Product: sen66 +# Model-Version: 1.7.1 +# + +import argparse +import time +from sensirion_i2c_driver import I2cConnection, CrcCalculator +from sensirion_shdlc_driver import ShdlcSerialPort, ShdlcConnection +from sensirion_shdlc_sensorbridge import (SensorBridgePort, + SensorBridgeShdlcDevice, + SensorBridgeI2cProxy) +from sensirion_driver_adapters.i2c_adapter.i2c_channel import I2cChannel +from sensirion_i2c_sen66.device import Sen66Device + +parser = argparse.ArgumentParser() +parser.add_argument('--serial-port', '-p', default='COM1') +args = parser.parse_args() + +with ShdlcSerialPort(port=args.serial_port, baudrate=460800) as port: + bridge = SensorBridgeShdlcDevice(ShdlcConnection(port), slave_address=0) + bridge.set_i2c_frequency(SensorBridgePort.ONE, frequency=100e3) + bridge.set_supply_voltage(SensorBridgePort.ONE, voltage=3.3) + bridge.switch_supply_on(SensorBridgePort.ONE) + i2c_transceiver = SensorBridgeI2cProxy(bridge, port=SensorBridgePort.ONE) + channel = I2cChannel(I2cConnection(i2c_transceiver), + slave_address=0x6B, + crc=CrcCalculator(8, 0x31, 0xff, 0x0)) + sensor = Sen66Device(channel) + try: + sensor.stop_measurement() + time.sleep(0.05) + + except: # noqa + print("stop measurement not successful" + ) + + sensor.start_continuous_measurement() + time.sleep(1.0) + for i in range(100): + (padding, data_ready + ) = sensor.get_data_ready() + if data_ready: + + # Readout raw data from the sensor + (raw_humidity, raw_temperature, raw_voc, raw_nox, raw_co2 + ) = sensor.read_measured_raw_values() + print(f"Raw humidity: {raw_humidity}" + ) + print(f"Raw temperature: {raw_temperature}" + ) + print(f"Raw VOC index: {raw_voc}" + ) + print(f"Raw NOX index: {raw_nox}" + ) + print(f"Raw CO₂: {raw_co2}" + ) diff --git a/examples/example_usage_linux_sen66.py b/examples/example_usage_linux_sen66.py index b25b6d5..5327aaa 100644 --- a/examples/example_usage_linux_sen66.py +++ b/examples/example_usage_linux_sen66.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # import argparse @@ -31,6 +31,7 @@ print(f"serial_number: {serial_number}; " ) sensor.start_continuous_measurement() + time.sleep(1.1) for i in range(100): try: time.sleep(1.0) diff --git a/examples/example_usage_sensorbridge_sen66.py b/examples/example_usage_sensorbridge_sen66.py index 27c21e6..b5f4c2a 100644 --- a/examples/example_usage_sensorbridge_sen66.py +++ b/examples/example_usage_sensorbridge_sen66.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # import argparse @@ -40,6 +40,7 @@ print(f"serial_number: {serial_number}; " ) sensor.start_continuous_measurement() + time.sleep(1.1) for i in range(100): try: time.sleep(1.0) diff --git a/images/product-image-sen6x.png b/images/product-image-sen6x.png new file mode 100644 index 0000000..b97bde3 Binary files /dev/null and b/images/product-image-sen6x.png differ diff --git a/images/product-pinout-sen6x.png b/images/product-pinout-sen6x.png new file mode 100644 index 0000000..a940971 Binary files /dev/null and b/images/product-pinout-sen6x.png differ diff --git a/metadata.yml b/metadata.yml index fd42221..cfe9140 100644 --- a/metadata.yml +++ b/metadata.yml @@ -1,7 +1,7 @@ # driver generation metadata -generator_version: 1.1.2 -model_version: 1.6.0 +generator_version: 1.7.0 +model_version: 1.7.1 dg_status: released is_manually_modified: false first_generated: '2024-10-30 08:14' -last_generated: '2025-03-13 10:43' +last_generated: '2026-06-05 11:22' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8e0dc33 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,68 @@ +[build-system] +requires = ["hatchling >= 1.26", "wheel >= 0.45.0"] +build-backend = "hatchling.build" + +[project] +name = "sensirion_i2c_sen66" +description = "I2C driver for the Sensirion SEN66 sensor family" + +readme = "README.md" +version = "1.3.0" + +requires-python = ">=3.8.4,<4.0" + +authors = [ + { name = "Sensirion", email = "info@sensirion.com" }, +] + +license = "BSD-3-Clause" +license-files = ["LICENSE"] + +keywords = [ + "Sensirion SEN66", + "I2C", + "SEN66", + ] + +classifiers = [ + "Intended Audience :: Developers", + "Topic :: System :: Hardware :: Hardware Drivers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +dependencies = [ + "sensirion-driver-adapters>=2.3.0,<3.0", + "sensirion-driver-support-types>=1.2.0,<2.0", + "sensirion-i2c-driver>=1.0,<2.0", + "sensirion-shdlc-sensorbridge>=0.1.0,<2.0" + ] + +[project.optional-dependencies] + +docs=[ + "jinja2~=3.1.6", + "sphinx-rtd-theme==3.0.2", + "sphinx>=7.0,<8.0;python_version < '3.11'", + "sphinx==8.2.3;python_version >= '3.11'", + "lazy-object-proxy ~=1.7.1", + "sphinx-autoapi~=3.0.0", +] + +test= [ + "flake8>=7.1.0", + "mock~=5.2.0", + "pytest>=8.3.5", + "pytest-cov>=5.0.0", + "mypy~=1.13.0", + "setuptools>=73.2.0" +] + +[project.urls] +Changelog = "https://github.com/Sensirion/python-i2c-sen66/blob/master/CHANGELOG.md" +Repository = "https://github.com/Sensirion/python-i2c-sen66" +Documentation = "https://sensirion.github.io/python-i2c-sen66" + diff --git a/sensirion_i2c_sen66/commands.py b/sensirion_i2c_sen66/commands.py index 5c0132b..ac19af8 100644 --- a/sensirion_i2c_sen66/commands.py +++ b/sensirion_i2c_sen66/commands.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # """ The transfer classes specify the data that is transferred between host and sensor. The generated transfer classes @@ -27,166 +27,24 @@ class DeviceStatus(BitfieldContainer): gas_error = BitField(offset=7, width=1) reserved3 = BitField(offset=8, width=1) co2_2_error = BitField(offset=9, width=1) - reserved4 = BitField(offset=10, width=1) + hcho_error = BitField(offset=10, width=1) pm_error = BitField(offset=11, width=1) - reserved5 = BitField(offset=12, width=1) - reserved6 = BitField(offset=13, width=8) + co2_1_error = BitField(offset=12, width=1) + reserved4 = BitField(offset=13, width=8) fan_speed_warning = BitField(offset=21, width=1) + reserved5 = BitField(offset=22, width=10) -class StartContinuousMeasurement(Transfer): - """ - Starts a continuous measurement. - After starting the measurement, it takes some time (~1.1s) until the - first measurement results are available. You could poll with the command - "Get Data Ready" to check when the results are ready to read. - This command is only available in idle mode. If the device is already - in any measure mode, this command has no effect. - """ - - CMD_ID = 0x21 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=0.05, slave_address=None, ignore_ack=False) - - -class StopMeasurement(Transfer): - """ - Stops the measurement and returns to idle mode. After sending this - command, wait at least 1000 ms before starting a new measurement. - If the device is already in idle mode, this command has no effect. - """ - - CMD_ID = 0x104 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=1.0, slave_address=None, ignore_ack=False) - - -class GetDataReady(Transfer): - """ - This command can be used to check if new measurement results are ready to read. The data ready flag - is automatically reset after reading the measurement values. - """ - - CMD_ID = 0x202 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>B?') - - -class ReadMeasuredValuesAsIntegers(Transfer): - """ - Returns the measured values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16, - 0x7FFF for int16). - """ - - CMD_ID = 0x300 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>HHHHhhhhH') - - -class ReadNumberConcentrationValuesAsIntegers(Transfer): - """ - Returns the measured number concentration values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16). - """ - - CMD_ID = 0x316 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>HHHHH') - - -class ReadMeasuredRawValues(Transfer): - """ - Returns the measured raw values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16, - 0x7FFF for int16). - """ - - CMD_ID = 0x405 - - def pack(self): - return self.tx_data.pack([]) - - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>hhHHH') - - -class StartFanCleaning(Transfer): - """ - This command triggers fan cleaning. The fan is set to the maximum - speed for 10 seconds and then automatically stopped. Wait at least 10s - after this command before starting a measurement. - """ +class GetVocAlgorithmTuningParameters(Transfer): + """Gets the parameters to customize the VOC algorithm.""" - CMD_ID = 0x5607 + CMD_ID = 0x60d0 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - - -class SetTemperatureOffsetParameters(Transfer): - """ - This command allows to compensate temperature effects of the - design-in at customer side by applying custom temperature offsets - to the ambient temperature. The compensated ambient temperature is - calculated as follows: - T_Ambient_Compensated = T_Ambient + (slope * T_Ambient) + offset - Where slope and offset are the values set with this command, - smoothed with the specified time constant. - All temperatures (T_Ambient_Compensated, T_Ambient and offset) - are represented in °C. - There are 5 temperature offset slots available that all contribute - additively to T_Ambient_Compensated. The default values for - the temperature offset parameters are all zero, meaning that - T_Ambient_Compensated is equal to T_Ambient by default. - The parameters can be changed in any state of the device, i.e. both in - idle mode and in measure mode. - """ - - CMD_ID = 0x60b2 - - def __init__(self, offset, slope, time_constant, slot): - self._offset = offset - self._slope = slope - self._time_constant = time_constant - self._slot = slot - - def pack(self): - return self.tx_data.pack([self._offset, self._slope, self._time_constant, self._slot]) - - tx = TxData(CMD_ID, '>HhhHH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>hhhhhh') class SetVocAlgorithmTuningParameters(Transfer): @@ -214,41 +72,42 @@ def pack(self): tx = TxData(CMD_ID, '>Hhhhhhh', device_busy_delay=0.02, slave_address=None, ignore_ack=False) -class GetVocAlgorithmTuningParameters(Transfer): - """Gets the parameters to customize the VOC algorithm.""" +class GetVocAlgorithmState(Transfer): + """ + Gets the current VOC algorithm state. + Allows to backup and restore the VOC algorithm state to resume operation after a power cycle or + device reset, skipping initial learning phase. By default, the VOC Engine is reset and the algorithm + state is retained if a measurement is stopped and started again. If the VOC algorithm + state shall be reset, a device reset or a power cycle can be executed. + """ - CMD_ID = 0x60d0 + CMD_ID = 0x6181 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>hhhhhh') + rx = RxData('>8B') -class SetNoxAlgorithmTuningParameters(Transfer): +class SetVocAlgorithmState(Transfer): """ - Sets the parameters to customize the NOx algorithm. - This configuration is volatile, i.e. the parameters will be - reverted to their default values after a device reset. + Sets the VOC algorithm state previously received with "Get VOC Algorithm State" command. + Allows restoration of the VOC algorithm state to resume operation after a power cycle or device + reset, skipping initial learning phase. By default, the VOC Engine is reset, and the algorithm state is retained if + a measurement is stopped and started again. If the VOC algorithm state shall be reset, a device reset, or a + power cycle can be executed. """ - CMD_ID = 0x60e1 + CMD_ID = 0x6181 - def __init__(self, index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, - std_initial, gain_factor): - self._index_offset = index_offset - self._learning_time_offset_hours = learning_time_offset_hours - self._learning_time_gain_hours = learning_time_gain_hours - self._gating_max_duration_minutes = gating_max_duration_minutes - self._std_initial = std_initial - self._gain_factor = gain_factor + def __init__(self, state): + self._state = state def pack(self): - return self.tx_data.pack([self._index_offset, self._learning_time_offset_hours, self._learning_time_gain_hours, - self._gating_max_duration_minutes, self._std_initial, self._gain_factor]) + return self.tx_data.pack([self._state]) - tx = TxData(CMD_ID, '>Hhhhhhh', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + tx = TxData(CMD_ID, '>H8B') class GetNoxAlgorithmTuningParameters(Transfer): @@ -263,81 +122,83 @@ def pack(self): rx = RxData('>hhhhhh') -class SetTemperatureAccelerationParameters(Transfer): +class SetNoxAlgorithmTuningParameters(Transfer): """ - This command allows to set custom temperature acceleration parameters of the RH/T engine. - It overwrites the default temperature acceleration parameters of the RH/T engine with custom values. This - configuration is volatile, i.e. the parameters will be reverted to their default values after a device reset. + Sets the parameters to customize the NOx algorithm. + This configuration is volatile, i.e. the parameters will be + reverted to their default values after a device reset. """ - CMD_ID = 0x6100 + CMD_ID = 0x60e1 - def __init__(self, k, p, t1, t2): - self._k = k - self._p = p - self._t1 = t1 - self._t2 = t2 + def __init__(self, index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, + std_initial, gain_factor): + self._index_offset = index_offset + self._learning_time_offset_hours = learning_time_offset_hours + self._learning_time_gain_hours = learning_time_gain_hours + self._gating_max_duration_minutes = gating_max_duration_minutes + self._std_initial = std_initial + self._gain_factor = gain_factor def pack(self): - return self.tx_data.pack([self._k, self._p, self._t1, self._t2]) + return self.tx_data.pack([self._index_offset, self._learning_time_offset_hours, self._learning_time_gain_hours, + self._gating_max_duration_minutes, self._std_initial, self._gain_factor]) - tx = TxData(CMD_ID, '>HHHHH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + tx = TxData(CMD_ID, '>Hhhhhhh', device_busy_delay=0.02, slave_address=None, ignore_ack=False) -class SetVocAlgorithmState(Transfer): +class PerformForcedCo2Recalibration(Transfer): """ - Sets the VOC algorithm state previously received with "Get VOC Algorithm State" command. - Allows restoration of the VOC algorithm state to resume operation after a power cycle or device - reset, skipping initial learning phase. By default, the VOC Engine is reset, and the algorithm state is retained if - a measurement is stopped and started again. If the VOC algorithm state shall be reset, a device reset, or a - power cycle can be executed. + Execute the forced recalibration (FRC) of the CO₂. See the datasheet of the + SCD4x sensor for details how the forced recalibration shall be used. """ - CMD_ID = 0x6181 + CMD_ID = 0x6707 - def __init__(self, state): - self._state = state + def __init__(self, target_co2_concentration): + self._target_co2_concentration = target_co2_concentration def pack(self): - return self.tx_data.pack([self._state]) + return self.tx_data.pack([self._target_co2_concentration]) - tx = TxData(CMD_ID, '>H8B') + tx = TxData(CMD_ID, '>HH', device_busy_delay=0.5, slave_address=None, ignore_ack=False) + rx = RxData('>H') -class GetVocAlgorithmState(Transfer): +class PerformCo2SensorFactoryReset(Transfer): """ - Gets the current VOC algorithm state. - Allows to backup and restore the VOC algorithm state to resume operation after a power cycle or - device reset, skipping initial learning phase. By default, the VOC Engine is reset and the algorithm - state is retained if a measurement is stopped and started again. If the VOC algorithm - state shall be reset, a device reset or a power cycle can be executed. + This command resets all CO₂ sensor configuration settings + stored in the EEPROM and erases the FRC and ASC algorithm + history of the CO₂ sensor. + The configuration settings are CO₂ sensor temperature offset (cannot be + modified over I2C interface), sensor altitude and the ASC + enabled/disabled parameters which are by default stored + in the volatile memory (RAM). + This command is only available in idle mode. """ - CMD_ID = 0x6181 + CMD_ID = 0x6754 def pack(self): return self.tx_data.pack([]) - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>8B') + tx = TxData(CMD_ID, '>H', device_busy_delay=1.4, slave_address=None, ignore_ack=False) -class PerformForcedCo2Recalibration(Transfer): +class GetCo2SensorAutomaticSelfCalibration(Transfer): """ - Execute the forced recalibration (FRC) of the CO₂. See the datasheet of the - SCD4x sensor for details how the forced recalibration shall be used. + The CO₂ sensor supports automatic self calibration (ASC) for long-term + stability of the CO₂ output. This feature can be enabled or disabled. + By default it is enabled. """ - CMD_ID = 0x6707 - - def __init__(self, target_co2_concentration): - self._target_co2_concentration = target_co2_concentration + CMD_ID = 0x6711 def pack(self): - return self.tx_data.pack([self._target_co2_concentration]) + return self.tx_data.pack([]) - tx = TxData(CMD_ID, '>HH', device_busy_delay=0.5, slave_address=None, ignore_ack=False) - rx = RxData('>H') + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>B?') class SetCo2SensorAutomaticSelfCalibration(Transfer): @@ -346,8 +207,6 @@ class SetCo2SensorAutomaticSelfCalibration(Transfer): The CO₂ sensor supports automatic self calibration (ASC) for long-term stability of the CO₂ output. This feature can be enabled or disabled. By default it is enabled. - This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device restart. """ CMD_ID = 0x6711 @@ -361,22 +220,20 @@ def pack(self): tx = TxData(CMD_ID, '>HH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) -class GetCo2SensorAutomaticSelfCalibration(Transfer): +class GetAmbientPressure(Transfer): """ - The CO₂ sensor supports automatic self calibration (ASC) for long-term - stability of the CO₂ output. This feature can be enabled or disabled. - By default it is enabled. - This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device restart. + Gets the ambient pressure value. + The ambient pressure can be used for pressure compensation in the CO₂ + sensor. """ - CMD_ID = 0x6711 + CMD_ID = 0x6720 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>B?') + rx = RxData('>H') class SetAmbientPressure(Transfer): @@ -402,14 +259,14 @@ def pack(self): tx = TxData(CMD_ID, '>HH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) -class GetAmbientPressure(Transfer): +class GetSensorAltitude(Transfer): """ - Gets the ambient pressure value. - The ambient pressure can be used for pressure compensation in the CO₂ + Gets the current sensor altitude. + The sensor altitude can be used for pressure compensation in the CO₂ sensor. """ - CMD_ID = 0x6720 + CMD_ID = 0x6736 def pack(self): return self.tx_data.pack([]) @@ -438,62 +295,144 @@ def pack(self): tx = TxData(CMD_ID, '>HH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) -class GetSensorAltitude(Transfer): +class StartContinuousMeasurement(Transfer): """ - Gets the current sensor altitude. - The sensor altitude can be used for pressure compensation in the CO₂ - sensor. + Starts a continuous measurement. + After starting the measurement, it takes some time (~1.1s) until the + first measurement results are available. You could poll with the command + "Get Data Ready" to check when the results are ready to read. + This command is only available in idle mode. If the device is already + in any measure mode, this command has no effect. """ - CMD_ID = 0x6736 + CMD_ID = 0x21 def pack(self): return self.tx_data.pack([]) - tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>H') + tx = TxData(CMD_ID, '>H', device_busy_delay=0.05, slave_address=None, ignore_ack=False) -class ActivateShtHeater(Transfer): +class StopMeasurement(Transfer): """ - This command allows to use the inbuilt heater in SHT sensor - to reverse creep at high humidity. - This command activates the SHT sensor heater with 200mW for 1s. - The heater is then automatically deactivated again. - The "get_sht_heater_measurements" command can be used to check if the - heater has finished (firmware version >= 4.0). - Wait at least 20s after this command before starting a measurement to get - coherent temperature values (heating consequence to disappear). + Stops the measurement and returns to idle mode. After sending this + command, wait at least 1000 ms before starting a new measurement. + If the device is already in idle mode, this command has no effect. """ - CMD_ID = 0x6765 + CMD_ID = 0x104 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=1.4, slave_address=None, ignore_ack=False) + + +class GetDataReady(Transfer): + """ + This command can be used to check if new measurement results are ready to read. The data ready flag + is automatically reset after reading the measurement values. + """ + + CMD_ID = 0x202 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>B?') -class GetShtHeaterMeasurements(Transfer): +class ReadNumberConcentrationValuesAsIntegers(Transfer): """ - Get the measured values when the SHT sensor heating is triggerd. If the - heating is not finished, the returned humidity and temperature values - are 0x7FFF. + Returns the measured number concentration values. + The command 0x0202 "Get Data Ready" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16). """ - CMD_ID = 0x6790 + CMD_ID = 0x316 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>hh') + rx = RxData('>HHHHH') -class GetProductName(Transfer): - """Gets the product name from the device.""" +class SetTemperatureOffsetParameters(Transfer): + """ + This command allows to compensate temperature effects of the + design-in at customer side by applying custom temperature offsets + to the ambient temperature. - CMD_ID = 0xd014 + The compensated ambient temperature is calculated as follows: + + * T_Ambient_Compensated = T_Ambient + (slope * T_Ambient) + offset + + Where \"slope\" and \"offset\" are the values set with this command, + smoothed with the specified time constant. + All temperatures (\"T_Ambient_Compensated\", \"T_Ambient\" and \"offset\") + are represented in °C. + There are 5 temperature offset slots available that all contribute + additively to \"T_Ambient_Compensated\". The default values for + the temperature offset parameters are all zero, meaning that + \"T_Ambient_Compensated\" is equal to \"T_Ambient\" by default. + The parameters can be changed in any state of the device, i.e. both in + idle mode and in measure mode. + """ + + CMD_ID = 0x60b2 + + def __init__(self, offset, slope, time_constant, slot): + self._offset = offset + self._slope = slope + self._time_constant = time_constant + self._slot = slot + + def pack(self): + return self.tx_data.pack([self._offset, self._slope, self._time_constant, self._slot]) + + tx = TxData(CMD_ID, '>HhhHH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + + +class SetTemperatureAccelerationParameters(Transfer): + """ + This command allows to set custom temperature acceleration parameters of the RH/T engine. + It overwrites the default temperature acceleration parameters of the RH/T engine with custom values. This + configuration is volatile, i.e. the parameters will be reverted to their default values after a device reset. + """ + + CMD_ID = 0x6100 + + def __init__(self, k, p, t1, t2): + self._k = k + self._p = p + self._t1 = t1 + self._t2 = t2 + + def pack(self): + return self.tx_data.pack([self._k, self._p, self._t1, self._t2]) + + tx = TxData(CMD_ID, '>HHHHH', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + + +class GetProductType(Transfer): + """ + Gets the product type from the device. + + The following product types are expected: + - SEN62: '00085800' + - SEN63C: '00085700' + - SEN65: '00085200' + - SEN66: '00085300' + - SEN68: '00085400' + - SEN69C: '00085900' + """ + + CMD_ID = 0xd002 def pack(self): return self.tx_data.pack([]) @@ -502,10 +441,10 @@ def pack(self): rx = RxData('>32s') -class GetSerialNumber(Transfer): - """Gets the serial number from the device.""" +class GetProductName(Transfer): + """Gets the product name from the device.""" - CMD_ID = 0xd033 + CMD_ID = 0xd014 def pack(self): return self.tx_data.pack([]) @@ -514,16 +453,16 @@ def pack(self): rx = RxData('>32s') -class GetVersion(Transfer): - """Gets the version information for the hardware, firmware and communication protocol.""" +class GetSerialNumber(Transfer): + """Gets the serial number from the device.""" - CMD_ID = 0xd100 + CMD_ID = 0xd033 def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) - rx = RxData('>BB') + rx = RxData('>32s') class ReadDeviceStatus(Transfer): @@ -561,6 +500,18 @@ def pack(self): rx = RxData('>I') +class GetVersion(Transfer): + """Gets the version information for the firmware.""" + + CMD_ID = 0xd100 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>BB') + + class DeviceReset(Transfer): """Executes a reset on the device. This has the same effect as a power cycle.""" @@ -570,3 +521,95 @@ def pack(self): return self.tx_data.pack([]) tx = TxData(CMD_ID, '>H', device_busy_delay=1.2, slave_address=None, ignore_ack=False) + + +class StartFanCleaning(Transfer): + """ + This command triggers fan cleaning. The fan is set to the maximum + speed for 10 seconds and then automatically stopped. Wait at least 10s + after this command before starting a measurement. + """ + + CMD_ID = 0x5607 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + + +class ActivateShtHeater(Transfer): + """ + Activate the heater feature of the SHT4x sensor. + This command allows to use the inbuilt heater in SHT sensor + to decontaminate and reverse creep at high humidity. + This command activates the SHT sensor heater with 200mW for 1s. + The SHT heater measurement done just before deactivation can be + read using the command \"Get SHT Heater Measurements\" after + the duration of the heating feature as specified in the SHT4x + datasheet. + Wait at least 20s after this command before starting a measurement + to get coherent temperature values (heating consequence to disappear). + """ + + CMD_ID = 0x6765 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + + +class GetShtHeaterMeasurements(Transfer): + """ + Get the measurement values when the SHT sensor heating + is finished. + """ + + CMD_ID = 0x6790 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>hh') + + +class ReadMeasuredValuesAsIntegers(Transfer): + """ + Returns the measured values. + The command \"Get Data Ready\" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16, + 0x7FFF for int16). + """ + + CMD_ID = 0x414 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>HHHHhhhhH') + + +class ReadMeasuredRawValues(Transfer): + """ + Returns the measured raw values. + The command "Get Data Ready" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16, + 0x7FFF for int16). + """ + + CMD_ID = 0x405 + + def pack(self): + return self.tx_data.pack([]) + + tx = TxData(CMD_ID, '>H', device_busy_delay=0.02, slave_address=None, ignore_ack=False) + rx = RxData('>hhHHH') diff --git a/sensirion_i2c_sen66/device.py b/sensirion_i2c_sen66/device.py index 39ba589..a70d8bf 100644 --- a/sensirion_i2c_sen66/device.py +++ b/sensirion_i2c_sen66/device.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # """ The class Sen66DeviceBase implements the low level interface of the sensor. @@ -19,9 +19,10 @@ from sensirion_driver_support_types.mixin_access import MixinAccess from sensirion_i2c_sen66.commands import (ActivateShtHeater, DeviceReset, DeviceStatus, GetAmbientPressure, GetCo2SensorAutomaticSelfCalibration, GetDataReady, - GetNoxAlgorithmTuningParameters, GetProductName, GetSensorAltitude, - GetSerialNumber, GetShtHeaterMeasurements, GetVersion, GetVocAlgorithmState, - GetVocAlgorithmTuningParameters, PerformForcedCo2Recalibration, + GetNoxAlgorithmTuningParameters, GetProductName, GetProductType, + GetSensorAltitude, GetSerialNumber, GetShtHeaterMeasurements, GetVersion, + GetVocAlgorithmState, GetVocAlgorithmTuningParameters, + PerformCo2SensorFactoryReset, PerformForcedCo2Recalibration, ReadAndClearDeviceStatus, ReadDeviceStatus, ReadMeasuredRawValues, ReadMeasuredValuesAsIntegers, ReadNumberConcentrationValuesAsIntegers, SetAmbientPressure, SetCo2SensorAutomaticSelfCalibration, @@ -30,12 +31,8 @@ SetVocAlgorithmState, SetVocAlgorithmTuningParameters, StartContinuousMeasurement, StartFanCleaning, StopMeasurement) -from sensirion_i2c_sen66.result_types import (SignalCo2, SignalHumidity, SignalMassConcentrationPm10p0, - SignalMassConcentrationPm1p0, SignalMassConcentrationPm2p5, - SignalMassConcentrationPm4p0, SignalNoxIndex, - SignalNumberConcentrationPm0p5, SignalNumberConcentrationPm10p0, - SignalNumberConcentrationPm1p0, SignalNumberConcentrationPm2p5, - SignalNumberConcentrationPm4p0, SignalTemperature, SignalVocIndex) +from sensirion_i2c_sen66.result_types import (SignalDividedBy10Int16, SignalDividedBy10Uint16, SignalHumidity, + SignalTemperature) class Sen66DeviceBase: @@ -48,203 +45,31 @@ def __init__(self, channel): def channel(self): return self._channel - def start_continuous_measurement(self): - """ - Starts a continuous measurement. - After starting the measurement, it takes some time (~1.1s) until the - first measurement results are available. You could poll with the command - "Get Data Ready" to check when the results are ready to read. - This command is only available in idle mode. If the device is already - in any measure mode, this command has no effect. - """ - transfer = StartContinuousMeasurement() - return execute_transfer(self._channel, transfer) - - def stop_measurement(self): - """ - Stops the measurement and returns to idle mode. After sending this - command, wait at least 1000 ms before starting a new measurement. - If the device is already in idle mode, this command has no effect. - """ - transfer = StopMeasurement() - return execute_transfer(self._channel, transfer) - - def get_data_ready(self): - """ - This command can be used to check if new measurement results are ready to read. The data ready flag - is automatically reset after reading the measurement values. - - :return padding: - Padding byte, always 0x00. - :return data_ready: - True (0x01) if data is ready, False (0x00) if not. When no measurement is running, False will be - returned. - """ - transfer = GetDataReady() - return execute_transfer(self._channel, transfer) - - def read_measured_values_as_integers(self): - """ - Returns the measured values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16, - 0x7FFF for int16). - - :return mass_concentration_pm1p0: - Value is scaled with factor 10: PM1.0 [µg/m³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return mass_concentration_pm2p5: - Value is scaled with factor 10: PM2.5 [µg/m³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return mass_concentration_pm4p0: - Value is scaled with factor 10: PM4.0 [µg/m³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return mass_concentration_pm10p0: - Value is scaled with factor 10: PM10.0 [µg/m³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return ambient_humidity: - Value is scaled with factor 100: RH [%] = value / 100 - *Note: If this value is unknown, 0x7FFF is returned.* - :return ambient_temperature: - Value is scaled with factor 200: T [°C] = value / 200 - *Note: If this value is unknown, 0x7FFF is returned.* - :return voc_index: - Value is scaled with factor 10: VOC Index = value / 10 - *Note: If this value is unknown, 0x7FFF is returned.* - :return nox_index: - Value is scaled with factor 10: NOx Index = value / 10 - *Note: If this value is unknown, 0x7FFF is returned. During - the first 10..11 seconds after power-on or device reset, this - value will be 0x7FFF as well.* - :return co2: - CO₂ concentration [ppm] - *Note: If this value is unknown, 0xFFFF is returned. During the - first 5..6 seconds after power-on or device reset, this value - will be 0xFFFF as well.* - """ - transfer = ReadMeasuredValuesAsIntegers() - return execute_transfer(self._channel, transfer) - - def read_number_concentration_values_as_integers(self): - """ - Returns the measured number concentration values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16). - - :return number_concentration_pm0p5: - Value is scaled with factor 10: PM0.5 [particles/cm³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return number_concentration_pm1p0: - Value is scaled with factor 10: PM1.0 [particles/cm³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return number_concentration_pm2p5: - Value is scaled with factor 10: PM2.5 [particles/cm³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return number_concentration_pm4p0: - Value is scaled with factor 10: PM4.0 [particles/cm³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - :return number_concentration_pm10p0: - Value is scaled with factor 10: PM10.0 [particles/cm³] = value / 10 - *Note: If this value is unknown, 0xFFFF is returned.* - """ - transfer = ReadNumberConcentrationValuesAsIntegers() - return execute_transfer(self._channel, transfer) - - def read_measured_raw_values(self): - """ - Returns the measured raw values. - The command "Get Data Ready" can be used to check if new - data is available since the last read operation. If no new data is - available, the previous values will be returned again. If no data - is available at all (e.g. measurement not running for at least one - second), all values will be at their upper limit (0xFFFF for uint16, - 0x7FFF for int16). - - :return raw_humidity: - Value is scaled with factor 100: RH [%] = value / 100 - *Note: If this value is unknown, 0x7FFF is returned.* - :return raw_temperature: - Value is scaled with factor 200: T [°C] = value / 200 - *Note: If this value is unknown, 0x7FFF is returned.* - :return raw_voc: - Raw measured VOC ticks without scale factor. - *Note: If this value is unknown, 0xFFFF is returned.* - :return raw_nox: - Raw measured NOx ticks without scale factor. - *Note: If this value is unknown, 0xFFFF is returned. During - the first 10..11 seconds after power-on or device reset, this - value will be 0xFFFF as well.* - :return raw_co2: - Not interpolated CO₂ concentration [ppm] updated every five - seconds. - *Note: If this value is unknown, 0xFFFF is returned. During the - first 5..6 seconds after power-on or device reset, this value - will be 0xFFFF as well.* + def get_voc_algorithm_tuning_parameters(self): """ - transfer = ReadMeasuredRawValues() - return execute_transfer(self._channel, transfer) + Gets the parameters to customize the VOC algorithm. - def start_fan_cleaning(self): - """ - This command triggers fan cleaning. The fan is set to the maximum - speed for 10 seconds and then automatically stopped. Wait at least 10s - after this command before starting a measurement. + :return index_offset: + VOC index representing typical (average) conditions. + :return learning_time_offset_hours: + Time constant to estimate the VOC algorithm offset from the history in hours. Past events will be + forgotten after about twice the learning time. + :return learning_time_gain_hours: + Time constant to estimate the VOC algorithm gain from the history in hours. Past events will be + forgotten after about twice the learning time. + :return gating_max_duration_minutes: + Maximum duration of gating in minutes (freeze of estimator during high VOC index signal). Zero + disables the gating. + :return std_initial: + Initial estimate for standard deviation. Lower value boosts events during initial learning period, + but may result in larger device-to-device variations. + :return gain_factor: + Gain factor to amplify or to attenuate the VOC index output. .. note:: This command is only available in idle mode. """ - transfer = StartFanCleaning() - return execute_transfer(self._channel, transfer) - - def set_temperature_offset_parameters(self, offset, slope, time_constant, slot): - """ - This command allows to compensate temperature effects of the - design-in at customer side by applying custom temperature offsets - to the ambient temperature. The compensated ambient temperature is - calculated as follows: - T_Ambient_Compensated = T_Ambient + (slope * T_Ambient) + offset - Where slope and offset are the values set with this command, - smoothed with the specified time constant. - All temperatures (T_Ambient_Compensated, T_Ambient and offset) - are represented in °C. - There are 5 temperature offset slots available that all contribute - additively to T_Ambient_Compensated. The default values for - the temperature offset parameters are all zero, meaning that - T_Ambient_Compensated is equal to T_Ambient by default. - The parameters can be changed in any state of the device, i.e. both in - idle mode and in measure mode. - - :param offset: - Constant temperature offset scaled with factor 200 (T [°C] = value / 200). - :param slope: - Normalized temperature offset slope scaled with factor 10000 (applied factor = value / 10000). - :param time_constant: - The time constant determines how fast the new slope and offset will be applied. - After the specified value in seconds, 63% of the new slope and offset are applied. - A time constant of zero means the new values will be applied immediately - (within the next measure interval of 1 second). - :param slot: - The temperature offset slot to be modified. - Valid values are 0 .. 4. If the value is outside this range, - the parameters will not be applied. - - .. note:: - This configuration is volatile, i.e. the parameters will be reverted to their default value of zero - after a device reset. - - :Example: - .. code-block:: python - - sensor.set_temperature_offset_parameters(1, 10, 1, 0) - - """ - transfer = SetTemperatureOffsetParameters(offset, slope, time_constant, slot) + transfer = GetVocAlgorithmTuningParameters() return execute_transfer(self._channel, transfer) def set_voc_algorithm_tuning_parameters(self, index_offset, learning_time_offset_hours, learning_time_gain_hours, @@ -290,31 +115,68 @@ def set_voc_algorithm_tuning_parameters(self, index_offset, learning_time_offset gating_max_duration_minutes, std_initial, gain_factor) return execute_transfer(self._channel, transfer) - def get_voc_algorithm_tuning_parameters(self): + def get_voc_algorithm_state(self): """ - Gets the parameters to customize the VOC algorithm. + Gets the current VOC algorithm state. + Allows to backup and restore the VOC algorithm state to resume operation after a power cycle or + device reset, skipping initial learning phase. By default, the VOC Engine is reset and the algorithm + state is retained if a measurement is stopped and started again. If the VOC algorithm + state shall be reset, a device reset or a power cycle can be executed. + + :return state: + Current VOC algorithm state. + + .. note:: + This command can be used either in measure mode or in idle mode (which will then return the state at the + time when the measurement was stopped). In measure mode, the state can be read each measure interval to + always have the latest state available, even in case of a sudden power loss. + """ + transfer = GetVocAlgorithmState() + return execute_transfer(self._channel, transfer)[0] + + def set_voc_algorithm_state(self, state): + """ + Sets the VOC algorithm state previously received with "Get VOC Algorithm State" command. + Allows restoration of the VOC algorithm state to resume operation after a power cycle or device + reset, skipping initial learning phase. By default, the VOC Engine is reset, and the algorithm state is retained if + a measurement is stopped and started again. If the VOC algorithm state shall be reset, a device reset, or a + power cycle can be executed. + + :param state: + VOC algorithm state to restore. + + .. note:: + This command is only available in idle mode and the state will be applied only once when starting + the next measurement. In measure mode, this command has no effect. + """ + transfer = SetVocAlgorithmState(state) + return execute_transfer(self._channel, transfer) + + def get_nox_algorithm_tuning_parameters(self): + """ + Gets the parameters to customize the NOx algorithm. :return index_offset: - VOC index representing typical (average) conditions. + NOx index representing typical (average) conditions. :return learning_time_offset_hours: - Time constant to estimate the VOC algorithm offset from the history in hours. Past events will be + Time constant to estimate the NOx algorithm offset from the history in hours. Past events will be forgotten after about twice the learning time. :return learning_time_gain_hours: - Time constant to estimate the VOC algorithm gain from the history in hours. Past events will be - forgotten after about twice the learning time. + The time constant to estimate the NOx algorithm gain from the history has no impact for NOx. This + parameter is still in place for consistency reasons with the VOC tuning parameters command. :return gating_max_duration_minutes: - Maximum duration of gating in minutes (freeze of estimator during high VOC index signal). Zero + Maximum duration of gating in minutes (freeze of estimator during high NOx index signal). Zero disables the gating. :return std_initial: - Initial estimate for standard deviation. Lower value boosts events during initial learning period, - but may result in larger device-to-device variations. + The initial estimate for standard deviation has no impact for NOx. This parameter is still in place + for consistency reasons with the VOC tuning parameters command. :return gain_factor: - Gain factor to amplify or to attenuate the VOC index output. + Gain factor to amplify or to attenuate the NOx index output. .. note:: - This command is only available in idle mode. + This command is available only in idle mode. """ - transfer = GetVocAlgorithmTuningParameters() + transfer = GetNoxAlgorithmTuningParameters() return execute_transfer(self._channel, transfer) def set_nox_algorithm_tuning_parameters(self, index_offset, learning_time_offset_hours, learning_time_gain_hours, @@ -360,285 +222,322 @@ def set_nox_algorithm_tuning_parameters(self, index_offset, learning_time_offset gating_max_duration_minutes, std_initial, gain_factor) return execute_transfer(self._channel, transfer) - def get_nox_algorithm_tuning_parameters(self): + def perform_forced_co2_recalibration(self, target_co2_concentration): """ - Gets the parameters to customize the NOx algorithm. + Execute the forced recalibration (FRC) of the CO₂. See the datasheet of the + SCD4x sensor for details how the forced recalibration shall be used. - :return index_offset: - NOx index representing typical (average) conditions. - :return learning_time_offset_hours: - Time constant to estimate the NOx algorithm offset from the history in hours. Past events will be - forgotten after about twice the learning time. - :return learning_time_gain_hours: - The time constant to estimate the NOx algorithm gain from the history has no impact for NOx. This - parameter is still in place for consistency reasons with the VOC tuning parameters command. - :return gating_max_duration_minutes: - Maximum duration of gating in minutes (freeze of estimator during high NOx index signal). Zero - disables the gating. - :return std_initial: - The initial estimate for standard deviation has no impact for NOx. This parameter is still in place - for consistency reasons with the VOC tuning parameters command. - :return gain_factor: - Gain factor to amplify or to attenuate the NOx index output. + :param target_co2_concentration: + Target CO₂ concentration [ppm] of the test setup. + + :return correction: + Correction value as received from the SCD [ppm CO₂]. + FRC correction [ppm CO₂] is calculated as follows: + FRC = return_value - 0x8000 + If the recalibration has failed this returned value + is 0xFFFF. .. note:: - This command is available only in idle mode. + After power-on wait at least 1000 ms and after stopping a measurement 600 ms before sending this + command. This command is not available in measure mode. The recalibration procedure will take about + 500 ms to complete, during which time no other functions can be executed. + This configuration is persistent, i.e. the parameters will be retained during a device reset or power cycle. """ - transfer = GetNoxAlgorithmTuningParameters() + transfer = PerformForcedCo2Recalibration(target_co2_concentration) + return execute_transfer(self._channel, transfer)[0] + + def perform_co2_sensor_factory_reset(self): + """ + This command resets all CO₂ sensor configuration settings + stored in the EEPROM and erases the FRC and ASC algorithm + history of the CO₂ sensor. + The configuration settings are CO₂ sensor temperature offset (cannot be + modified over I2C interface), sensor altitude and the ASC + enabled/disabled parameters which are by default stored + in the volatile memory (RAM). + This command is only available in idle mode. + """ + transfer = PerformCo2SensorFactoryReset() return execute_transfer(self._channel, transfer) - def set_temperature_acceleration_parameters(self, k, p, t1, t2): + def get_co2_sensor_automatic_self_calibration(self): """ - This command allows to set custom temperature acceleration parameters of the RH/T engine. - It overwrites the default temperature acceleration parameters of the RH/T engine with custom values. This - configuration is volatile, i.e. the parameters will be reverted to their default values after a device reset. + The CO₂ sensor supports automatic self calibration (ASC) for long-term + stability of the CO₂ output. This feature can be enabled or disabled. + By default it is enabled. - :param k: - Filter constant K scaled with factor 10 (K = value / 10). - :param p: - Filter constant P scaled with factor 10 (P = value / 10). - :param t1: - Time constant T1 scaled with factor 10 (T1 [s] = value / 10). - :param t2: - Time constant T2 scaled with factor 10 (T2 [s] = value / 10). + :return padding: + Padding byte, always 0x00. + :return status: + Is set true (0x01) if the automatic self calibration is enabled or false (0x00) if the automatic + self calibration is disabled. .. note:: - The command is only available in idle mode. + This command is only available in idle mode. + This configuration is persistent, i.e. the status will be retained during a device reset or power cycle. + """ + transfer = GetCo2SensorAutomaticSelfCalibration() + return execute_transfer(self._channel, transfer) + + def set_co2_sensor_automatic_self_calibration(self, status): + """ + Sets the status of the CO₂ sensor automatic self-calibration (ASC). + The CO₂ sensor supports automatic self calibration (ASC) for long-term + stability of the CO₂ output. This feature can be enabled or disabled. + By default it is enabled. + + :param status: + Set to true (0x0001) to enable or false (0x0000) to disable the automatic CO₂ measurement self + calibration feature. High byte of uint16 is padding and always 0x00. + + .. note:: + This command is only available in idle mode. + This configuration is persistent, i.e. the status will be retained during a device reset or power cycle. :Example: .. code-block:: python - sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) + sensor.set_co2_sensor_automatic_self_calibration(0) """ - transfer = SetTemperatureAccelerationParameters(k, p, t1, t2) + transfer = SetCo2SensorAutomaticSelfCalibration(status) return execute_transfer(self._channel, transfer) - def set_voc_algorithm_state(self, state): + def get_ambient_pressure(self): """ - Sets the VOC algorithm state previously received with "Get VOC Algorithm State" command. - Allows restoration of the VOC algorithm state to resume operation after a power cycle or device - reset, skipping initial learning phase. By default, the VOC Engine is reset, and the algorithm state is retained if - a measurement is stopped and started again. If the VOC algorithm state shall be reset, a device reset, or a - power cycle can be executed. + Gets the ambient pressure value. + The ambient pressure can be used for pressure compensation in the CO₂ + sensor. - :param state: - VOC algorithm state to restore. + :return ambient_pressure: + Currently used ambient pressure [hPa] for pressure compensation. .. note:: - This command is only available in idle mode and the state will be applied only once when starting - the next measurement. In measure mode, this command has no effect. + This command can be used in any state of the device, i.e. both in idle + mode and in measure mode. """ - transfer = SetVocAlgorithmState(state) - return execute_transfer(self._channel, transfer) + transfer = GetAmbientPressure() + return execute_transfer(self._channel, transfer)[0] - def get_voc_algorithm_state(self): + def set_ambient_pressure(self, ambient_pressure): """ - Gets the current VOC algorithm state. - Allows to backup and restore the VOC algorithm state to resume operation after a power cycle or - device reset, skipping initial learning phase. By default, the VOC Engine is reset and the algorithm - state is retained if a measurement is stopped and started again. If the VOC algorithm - state shall be reset, a device reset or a power cycle can be executed. + The ambient pressure can be used for pressure compensation in the CO₂ + sensor. Setting an ambient pressure overrides any pressure compensation + based on a previously set sensor altitude. Use of this command is + recommended for applications experiencing significant ambient pressure + changes to ensure CO₂ sensor accuracy. Valid input values are between + 700 to 1'200 hPa. The default value is 1013 hPa. + This configuration is volatile, i.e. the parameter will be + reverted to its default value after a device restart. - :return state: - Current VOC algorithm state. + :param ambient_pressure: + Ambient pressure [hPa] to be used for pressure compensation. .. note:: - This command can be used either in measure mode or in idle mode (which will then return the state at the - time when the measurement was stopped). In measure mode, the state can be read each measure interval to - always have the latest state available, even in case of a sudden power loss. - """ - transfer = GetVocAlgorithmState() - return execute_transfer(self._channel, transfer)[0] + This command can be used in any state of the device, i.e. both in idle + mode and in measure mode. + + :Example: + .. code-block:: python + + sensor.set_ambient_pressure(1013) - def perform_forced_co2_recalibration(self, target_co2_concentration): """ - Execute the forced recalibration (FRC) of the CO₂. See the datasheet of the - SCD4x sensor for details how the forced recalibration shall be used. + transfer = SetAmbientPressure(ambient_pressure) + return execute_transfer(self._channel, transfer) - :param target_co2_concentration: - Target CO₂ concentration [ppm] of the test setup. + def get_sensor_altitude(self): + """ + Gets the current sensor altitude. + The sensor altitude can be used for pressure compensation in the CO₂ + sensor. - :return correction: - Correction value as received from the SCD [ppm CO₂]. - FRC correction [ppm CO₂] is calculated as follows: - FRC = return_value - 0x8000 - If the recalibration has failed this returned value - is 0xFFFF. + :return altitude: + Current sensor altitude [m]. .. note:: - After power-on wait at least 1000 ms and after stopping a measurement 600 ms before sending this - command. This command is not available in measure mode. The recalibration procedure will take about - 500 ms to complete, during which time no other functions can be executed. + This command is only available in idle mode. """ - transfer = PerformForcedCo2Recalibration(target_co2_concentration) + transfer = GetSensorAltitude() return execute_transfer(self._channel, transfer)[0] - def set_co2_sensor_automatic_self_calibration(self, status): + def set_sensor_altitude(self, altitude): """ - Sets the status of the CO₂ sensor automatic self-calibration (ASC). - The CO₂ sensor supports automatic self calibration (ASC) for long-term - stability of the CO₂ output. This feature can be enabled or disabled. - By default it is enabled. + The sensor altitude can be used for pressure compensation in the CO₂ + sensor. The default sensor altitude value is set to 0 meters above sea + level. Valid input values are between 0 and 3000m. This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device restart. - - :param status: - Set to true (0x0001) to enable or false (0x0000) to disable the automatic CO₂ measurement self - calibration feature. High byte of uint16 is padding and always 0x00. + reverted to its default value after a device reset. - .. note:: - This command is only available in idle mode. + :param altitude: + Sensor altitude [m], valid input between 0 and 3000m. :Example: .. code-block:: python - sensor.set_co2_sensor_automatic_self_calibration(0) + sensor.set_sensor_altitude(0) """ - transfer = SetCo2SensorAutomaticSelfCalibration(status) + transfer = SetSensorAltitude(altitude) return execute_transfer(self._channel, transfer) - def get_co2_sensor_automatic_self_calibration(self): + def start_continuous_measurement(self): """ - The CO₂ sensor supports automatic self calibration (ASC) for long-term - stability of the CO₂ output. This feature can be enabled or disabled. - By default it is enabled. - This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device restart. + Starts a continuous measurement. + After starting the measurement, it takes some time (~1.1s) until the + first measurement results are available. You could poll with the command + "Get Data Ready" to check when the results are ready to read. + This command is only available in idle mode. If the device is already + in any measure mode, this command has no effect. + """ + transfer = StartContinuousMeasurement() + return execute_transfer(self._channel, transfer) + + def stop_measurement(self): + """ + Stops the measurement and returns to idle mode. After sending this + command, wait at least 1000 ms before starting a new measurement. + If the device is already in idle mode, this command has no effect. + """ + transfer = StopMeasurement() + return execute_transfer(self._channel, transfer) + + def get_data_ready(self): + """ + This command can be used to check if new measurement results are ready to read. The data ready flag + is automatically reset after reading the measurement values. :return padding: Padding byte, always 0x00. - :return status: - Is set true (0x01) if the automatic self calibration is enabled or false (0x00) if the automatic - self calibration is disabled. + :return data_ready: + True (0x01) if data is ready, False (0x00) if not. When no measurement is running, False will be + returned. + """ + transfer = GetDataReady() + return execute_transfer(self._channel, transfer) - .. note:: - This command is only available in idle mode. + def read_number_concentration_values_as_integers(self): + """ + Returns the measured number concentration values. + The command 0x0202 "Get Data Ready" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16). + + :return number_concentration_pm0p5: + Value is scaled with factor 10: PM0.5 [particles/cm³] = value / 10. + *Note: If this value is unknown, 0xFFFF is returned.* + :return number_concentration_pm1p0: + Value is scaled with factor 10: PM1.0 [particles/cm³] = value / 10. + *Note: If this value is unknown, 0xFFFF is returned.* + :return number_concentration_pm2p5: + Value is scaled with factor 10: PM2.5 [particles/cm³] = value / 10. + *Note: If this value is unknown, 0xFFFF is returned.* + :return number_concentration_pm4p0: + Value is scaled with factor 10: PM4.0 [particles/cm³] = value / 10. + *Note: If this value is unknown, 0xFFFF is returned.* + :return number_concentration_pm10p0: + Value is scaled with factor 10: PM10.0 [particles/cm³] = value / 10. + *Note: If this value is unknown, 0xFFFF is returned.* """ - transfer = GetCo2SensorAutomaticSelfCalibration() + transfer = ReadNumberConcentrationValuesAsIntegers() return execute_transfer(self._channel, transfer) - def set_ambient_pressure(self, ambient_pressure): + def set_temperature_offset_parameters(self, offset, slope, time_constant, slot): """ - The ambient pressure can be used for pressure compensation in the CO₂ - sensor. Setting an ambient pressure overrides any pressure compensation - based on a previously set sensor altitude. Use of this command is - recommended for applications experiencing significant ambient pressure - changes to ensure CO₂ sensor accuracy. Valid input values are between - 700 to 1'200 hPa. The default value is 1013 hPa. - This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device restart. + This command allows to compensate temperature effects of the + design-in at customer side by applying custom temperature offsets + to the ambient temperature. - :param ambient_pressure: - Ambient pressure [hPa] to be used for pressure compensation. + The compensated ambient temperature is calculated as follows: + + * T_Ambient_Compensated = T_Ambient + (slope * T_Ambient) + offset + + Where \"slope\" and \"offset\" are the values set with this command, + smoothed with the specified time constant. + All temperatures (\"T_Ambient_Compensated\", \"T_Ambient\" and \"offset\") + are represented in °C. + There are 5 temperature offset slots available that all contribute + additively to \"T_Ambient_Compensated\". The default values for + the temperature offset parameters are all zero, meaning that + \"T_Ambient_Compensated\" is equal to \"T_Ambient\" by default. + The parameters can be changed in any state of the device, i.e. both in + idle mode and in measure mode. + + :param offset: + Constant temperature offset scaled with factor 200 (T [°C] = value / 200). + :param slope: + Normalized temperature offset slope scaled with factor 10000 (applied factor = value / 10000). + :param time_constant: + The time constant determines how fast the new slope and offset will be applied. + After the specified value in seconds, 63% of the new slope and offset are applied. + A time constant of zero means the new values will be applied immediately + (within the next measure interval of 1 second). + :param slot: + The temperature offset slot to be modified. + Valid values are 0 .. 4. If the value is outside this range, + the parameters will not be applied. .. note:: - This command can be used in any state of the device, i.e. both in idle - mode and in measure mode. + This configuration is volatile, i.e. the parameters will be reverted to their default value of zero + after a device reset. :Example: .. code-block:: python - sensor.set_ambient_pressure(1013) + sensor.set_temperature_offset_parameters(1, 10, 1, 0) """ - transfer = SetAmbientPressure(ambient_pressure) + transfer = SetTemperatureOffsetParameters(offset, slope, time_constant, slot) return execute_transfer(self._channel, transfer) - def get_ambient_pressure(self): + def set_temperature_acceleration_parameters(self, k, p, t1, t2): """ - Gets the ambient pressure value. - The ambient pressure can be used for pressure compensation in the CO₂ - sensor. + This command allows to set custom temperature acceleration parameters of the RH/T engine. + It overwrites the default temperature acceleration parameters of the RH/T engine with custom values. This + configuration is volatile, i.e. the parameters will be reverted to their default values after a device reset. - :return ambient_pressure: - Currently used ambient pressure [hPa] for pressure compensation. + :param k: + Filter constant K scaled with factor 10 (K = value / 10). + :param p: + Filter constant P scaled with factor 10 (P = value / 10). + :param t1: + Time constant T1 scaled with factor 10 (T1 [s] = value / 10). + :param t2: + Time constant T2 scaled with factor 10 (T2 [s] = value / 10). .. note:: - This command can be used in any state of the device, i.e. both in idle - mode and in measure mode. - """ - transfer = GetAmbientPressure() - return execute_transfer(self._channel, transfer)[0] - - def set_sensor_altitude(self, altitude): - """ - The sensor altitude can be used for pressure compensation in the CO₂ - sensor. The default sensor altitude value is set to 0 meters above sea - level. Valid input values are between 0 and 3000m. - This configuration is volatile, i.e. the parameter will be - reverted to its default value after a device reset. - - :param altitude: - Sensor altitude [m], valid input between 0 and 3000m. + The command is only available in idle mode. :Example: .. code-block:: python - sensor.set_sensor_altitude(0) + sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) """ - transfer = SetSensorAltitude(altitude) + transfer = SetTemperatureAccelerationParameters(k, p, t1, t2) return execute_transfer(self._channel, transfer) - def get_sensor_altitude(self): + def get_product_type(self): """ - Gets the current sensor altitude. - The sensor altitude can be used for pressure compensation in the CO₂ - sensor. + Gets the product type from the device. - :return altitude: - Current sensor altitude [m]. + The following product types are expected: + - SEN62: '00085800' + - SEN63C: '00085700' + - SEN65: '00085200' + - SEN66: '00085300' + - SEN68: '00085400' + - SEN69C: '00085900' - .. note:: - This command is only available in idle mode. + :return product_type: + Null-terminated ASCII string containing the product type. + Up to 32 characters can be read from the device. """ - transfer = GetSensorAltitude() + transfer = GetProductType() return execute_transfer(self._channel, transfer)[0] - def activate_sht_heater(self): - """ - This command allows to use the inbuilt heater in SHT sensor - to reverse creep at high humidity. - This command activates the SHT sensor heater with 200mW for 1s. - The heater is then automatically deactivated again. - The "get_sht_heater_measurements" command can be used to check if the - heater has finished (firmware version >= 4.0). - Wait at least 20s after this command before starting a measurement to get - coherent temperature values (heating consequence to disappear). - - .. note:: - This command is only available in idle mode. - For firmware version < 4.0, wait for at least 1300ms before sending another - command, to ensure heating is finsihed. - """ - transfer = ActivateShtHeater() - return execute_transfer(self._channel, transfer) - - def get_sht_heater_measurements(self): - """ - Get the measured values when the SHT sensor heating is triggerd. If the - heating is not finished, the returned humidity and temperature values - are 0x7FFF. - - :return humidity: - Value is scaled with factor 100: RH [%] = value / 100 - *Note: If this value is not available, 0x7FFF is returned.* - :return temperature: - Value is scaled with factor 200: T [°C] = value / 200 - *Note: If this value is not available, 0x7FFF is returned.* - - .. note:: - This command is only availble in idle mode. - This command is only available for firmware version >= 4.0. - This command must be used after the "activate_sht_heater" command. - The get_sht_heater_measurements command can be queried every 0.05s to get - the measurements. - """ - transfer = GetShtHeaterMeasurements() - return execute_transfer(self._channel, transfer) - def get_product_name(self): """ Gets the product name from the device. @@ -661,18 +560,6 @@ def get_serial_number(self): transfer = GetSerialNumber() return execute_transfer(self._channel, transfer)[0] - def get_version(self): - """ - Gets the version information for the hardware, firmware and communication protocol. - - :return firmware_major: - Firmware major version number. - :return firmware_minor: - Firmware minor version number. - """ - transfer = GetVersion() - return execute_transfer(self._channel, transfer) - def read_device_status(self): """ Reads the current device status. @@ -710,11 +597,157 @@ def read_and_clear_device_status(self): res_0 = execute_transfer(self._channel, transfer)[0] return DeviceStatus(res_0) + def get_version(self): + """ + Gets the version information for the firmware. + + :return firmware_major: + Firmware major version number. + :return firmware_minor: + Firmware minor version number. + """ + transfer = GetVersion() + return execute_transfer(self._channel, transfer) + def device_reset(self): """Executes a reset on the device. This has the same effect as a power cycle.""" transfer = DeviceReset() return execute_transfer(self._channel, transfer) + def start_fan_cleaning(self): + """ + This command triggers fan cleaning. The fan is set to the maximum + speed for 10 seconds and then automatically stopped. Wait at least 10s + after this command before starting a measurement. + + .. note:: + This command is only available in idle mode. + """ + transfer = StartFanCleaning() + return execute_transfer(self._channel, transfer) + + def activate_sht_heater(self): + """ + Activate the heater feature of the SHT4x sensor. + This command allows to use the inbuilt heater in SHT sensor + to decontaminate and reverse creep at high humidity. + This command activates the SHT sensor heater with 200mW for 1s. + The SHT heater measurement done just before deactivation can be + read using the command \"Get SHT Heater Measurements\" after + the duration of the heating feature as specified in the SHT4x + datasheet. + Wait at least 20s after this command before starting a measurement + to get coherent temperature values (heating consequence to disappear). + + .. note:: + This command is only available in idle mode. + """ + transfer = ActivateShtHeater() + return execute_transfer(self._channel, transfer) + + def get_sht_heater_measurements(self): + """ + Get the measurement values when the SHT sensor heating + is finished. + + :return sht_relative_humidity: + If the sht heating is completed, this value indicates + the scaled relative humidity of the SHT4x sensor. + Value is scaled with factor 100: RH [%] = value / 100 + *Note: If this value is not available, 0x7FFF is returned.* + :return sht_temperature: + If the sht heating is completed, this value indicates + the scaled temperature of the SHT4x sensor. + Value is scaled with factor 200: T [°C] = value / 200 + *Note: If this value is not available, 0x7FFF is returned.* + + .. note:: + This command must be used after the \"Activate SHT Heater\" command. The get sht heater measurements + command can be queried every 0.05s to get the measurements. This command is only available in idle + mode. + """ + transfer = GetShtHeaterMeasurements() + return execute_transfer(self._channel, transfer) + + def read_measured_values_as_integers(self): + """ + Returns the measured values. + The command \"Get Data Ready\" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16, + 0x7FFF for int16). + + :return mass_concentration_pm1p0: + Value is scaled with factor 10: PM1.0 [µg/m³] = value / 10 + *Note: If this value is unknown, 0xFFFF is returned.* + :return mass_concentration_pm2p5: + Value is scaled with factor 10: PM2.5 [µg/m³] = value / 10 + *Note: If this value is unknown, 0xFFFF is returned.* + :return mass_concentration_pm4p0: + Value is scaled with factor 10: PM4.0 [µg/m³] = value / 10 + *Note: If this value is unknown, 0xFFFF is returned.* + :return mass_concentration_pm10p0: + Value is scaled with factor 10: PM10.0 [µg/m³] = value / 10 + *Note: If this value is unknown, 0xFFFF is returned.* + :return ambient_humidity: + Value is scaled with factor 100: RH [%] = value / 100 + *Note: If this value is unknown, 0x7FFF is returned.* + :return ambient_temperature: + Value is scaled with factor 200: T [°C] = value / 200 + *Note: If this value is unknown, 0x7FFF is returned.* + :return voc_index: + Value is scaled with factor 10: VOC Index = value / 10 + *Note: If this value is unknown, 0x7FFF is returned.* + :return nox_index: + Value is scaled with factor 10: NOx Index = value / 10 + *Note: If this value is unknown, 0x7FFF is returned. During + the first 10..11 seconds after power-on or device reset, this + value will be 0x7FFF as well.* + :return co2: + CO₂ concentration [ppm] + *Note: If this value is unknown, 0xFFFF is returned. During the + first 5..6 seconds after power-on or device reset, this value + will be 0xFFFF as well.* + """ + transfer = ReadMeasuredValuesAsIntegers() + return execute_transfer(self._channel, transfer) + + def read_measured_raw_values(self): + """ + Returns the measured raw values. + The command "Get Data Ready" can be used to check if new + data is available since the last read operation. If no new data is + available, the previous values will be returned again. If no data + is available at all (e.g. measurement not running for at least one + second), all values will be at their upper limit (0xFFFF for uint16, + 0x7FFF for int16). + + :return raw_humidity: + Value is scaled with factor 100: RH [%] = value / 100 + *Note: If this value is unknown, 0x7FFF is returned.* + :return raw_temperature: + Value is scaled with factor 200: T [°C] = value / 200 + *Note: If this value is unknown, 0x7FFF is returned.* + :return raw_voc: + Raw measured VOC ticks without scale factor. + *Note: If this value is unknown, 0xFFFF is returned.* + :return raw_nox: + Raw measured NOx ticks without scale factor. + *Note: If this value is unknown, 0xFFFF is returned. During + the first 10..11 seconds after power-on or device reset, this + value will be 0xFFFF as well.* + :return raw_co2: + Not interpolated CO₂ concentration [ppm] updated every five + seconds. + *Note: If this value is unknown, 0xFFFF is returned. During the + first 5..6 seconds after power-on or device reset, this value + will be 0xFFFF as well.* + """ + transfer = ReadMeasuredRawValues() + return execute_transfer(self._channel, transfer) + class Sen66Device(Sen66DeviceBase): """Driver class implementation of SEN66""" @@ -740,24 +773,24 @@ def read_measured_values(self): :return humidity: Measured humidity in %RH. :return temperature: - Measured temperature in degrees celsius. + Measured temperature in degrees Celsius. :return voc_index: - Measured VOC Index between 0 and 500. + VOC Index :return nox_index: - Measured NOx Index between 0 and 500. + NOx Index :return co2: - Measured CO2 concentration in ppm. + CO2 concentration in ppm. """ (mass_concentration_pm1p0_raw, mass_concentration_pm2p5_raw, mass_concentration_pm4p0_raw, mass_concentration_pm10p0_raw, humidity_raw, temperature_raw, voc_index_raw, nox_index_raw, co2_raw ) = self.read_measured_values_as_integers() - return (SignalMassConcentrationPm1p0(mass_concentration_pm1p0_raw), - SignalMassConcentrationPm2p5(mass_concentration_pm2p5_raw), - SignalMassConcentrationPm4p0(mass_concentration_pm4p0_raw), - SignalMassConcentrationPm10p0(mass_concentration_pm10p0_raw), SignalHumidity(humidity_raw), - SignalTemperature(temperature_raw), SignalVocIndex(voc_index_raw), SignalNoxIndex(nox_index_raw), - SignalCo2(co2_raw)) + return (SignalDividedBy10Uint16(mass_concentration_pm1p0_raw), + SignalDividedBy10Uint16(mass_concentration_pm2p5_raw), + SignalDividedBy10Uint16(mass_concentration_pm4p0_raw), + SignalDividedBy10Uint16(mass_concentration_pm10p0_raw), SignalHumidity(humidity_raw), + SignalTemperature(temperature_raw), SignalDividedBy10Int16(voc_index_raw), + SignalDividedBy10Int16(nox_index_raw), co2_raw) def read_number_concentration_values(self): """ @@ -777,8 +810,8 @@ def read_number_concentration_values(self): (number_concentration_pm0p5_raw, number_concentration_pm1p0_raw, number_concentration_pm2p5_raw, number_concentration_pm4p0_raw, number_concentration_pm10p0_raw ) = self.read_number_concentration_values_as_integers() - return (SignalNumberConcentrationPm0p5(number_concentration_pm0p5_raw), - SignalNumberConcentrationPm1p0(number_concentration_pm1p0_raw), - SignalNumberConcentrationPm2p5(number_concentration_pm2p5_raw), - SignalNumberConcentrationPm4p0(number_concentration_pm4p0_raw), - SignalNumberConcentrationPm10p0(number_concentration_pm10p0_raw)) + return (SignalDividedBy10Uint16(number_concentration_pm0p5_raw), + SignalDividedBy10Uint16(number_concentration_pm1p0_raw), + SignalDividedBy10Uint16(number_concentration_pm2p5_raw), + SignalDividedBy10Uint16(number_concentration_pm4p0_raw), + SignalDividedBy10Uint16(number_concentration_pm10p0_raw)) diff --git a/sensirion_i2c_sen66/response_provider.py b/sensirion_i2c_sen66/response_provider.py index 95ac2e4..cd20a2a 100644 --- a/sensirion_i2c_sen66/response_provider.py +++ b/sensirion_i2c_sen66/response_provider.py @@ -4,7 +4,8 @@ class Sen66ResponseProvider(rp.ResponseProvider): - RESPONSE_MAP = {0xd014: struct.pack('>32s', rp.random_ascii_string(32)), + RESPONSE_MAP = {0xd002: struct.pack('>32s', rp.random_ascii_string(32)), + 0xd014: struct.pack('>32s', rp.random_ascii_string(32)), 0xd033: struct.pack('>32s', rp.random_ascii_string(32))} def get_id(self) -> str: diff --git a/sensirion_i2c_sen66/result_types.py b/sensirion_i2c_sen66/result_types.py index 19a8288..ef580f9 100644 --- a/sensirion_i2c_sen66/result_types.py +++ b/sensirion_i2c_sen66/result_types.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # """ The signal classes specify transformations of the raw sensor signals into a meaningful units. @@ -17,134 +17,42 @@ from sensirion_driver_support_types.signals import AbstractSignal -class SignalMassConcentrationPm1p0(AbstractSignal): - """Mass concentration in μg/m³ for particles smaller than 1.0 μm""" +class SignalDividedBy10Uint16(AbstractSignal): + """ + All mass and number concentrations are scaled by a factor 10 to avoid + floating point operations on the communication interface. + """ - def __init__(self, mass_concentration_pm1p0_raw): - self._mass_concentration_pm1p0 = mass_concentration_pm1p0_raw / 10.0 + def __init__(self, scaled_integer_value): + self._divided_by_10_uint16 = scaled_integer_value / 10.0 @property def value(self): - return self._mass_concentration_pm1p0 + return self._divided_by_10_uint16 def __str__(self): return '{0:.2f}'.format(self.value) -class SignalMassConcentrationPm2p5(AbstractSignal): - """Mass concentration in μg/m³ for particles smaller than 2.5 μm""" +class SignalDividedBy10Int16(AbstractSignal): + """ + The VOC and NOx indices are scaled by a factor 10 to avoid + floating point operations on the communication interface. + """ - def __init__(self, mass_concentration_pm2p5_raw): - self._mass_concentration_pm2p5 = mass_concentration_pm2p5_raw / 10.0 + def __init__(self, scaled_integer_value): + self._divided_by_10_int16 = scaled_integer_value / 10.0 @property def value(self): - return self._mass_concentration_pm2p5 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalMassConcentrationPm4p0(AbstractSignal): - """Mass concentration in μg/m³ for particles smaller than 4.0 μm""" - - def __init__(self, mass_concentration_pm4p0_raw): - self._mass_concentration_pm4p0 = mass_concentration_pm4p0_raw / 10.0 - - @property - def value(self): - return self._mass_concentration_pm4p0 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalMassConcentrationPm10p0(AbstractSignal): - """Mass concentration in μg/m³ for particles smaller than 10.0 μm""" - - def __init__(self, mass_concentration_pm10p0_raw): - self._mass_concentration_pm10p0 = mass_concentration_pm10p0_raw / 10.0 - - @property - def value(self): - return self._mass_concentration_pm10p0 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNumberConcentrationPm0p5(AbstractSignal): - """Number concentration in particles/cm³ for particles smaller than 0.5 μm""" - - def __init__(self, number_concentration_pm0p5_raw): - self._number_concentration_pm0p5 = number_concentration_pm0p5_raw / 10.0 - - @property - def value(self): - return self._number_concentration_pm0p5 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNumberConcentrationPm1p0(AbstractSignal): - """Number concentration in particles/cm³ for particles smaller than 1.0 μm""" - - def __init__(self, number_concentration_pm1p0_raw): - self._number_concentration_pm1p0 = number_concentration_pm1p0_raw / 10.0 - - @property - def value(self): - return self._number_concentration_pm1p0 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNumberConcentrationPm2p5(AbstractSignal): - """Number concentration in particles/cm³ for particles smaller than 2.5 μm""" - - def __init__(self, number_concentration_pm2p5_raw): - self._number_concentration_pm2p5 = number_concentration_pm2p5_raw / 10.0 - - @property - def value(self): - return self._number_concentration_pm2p5 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNumberConcentrationPm4p0(AbstractSignal): - """Number concentration in particles/cm³ for particles smaller than 4.0 μm""" - - def __init__(self, number_concentration_pm4p0_raw): - self._number_concentration_pm4p0 = number_concentration_pm4p0_raw / 10.0 - - @property - def value(self): - return self._number_concentration_pm4p0 - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNumberConcentrationPm10p0(AbstractSignal): - """Number concentration in particles/cm³ for particles smaller than 10.0 μm""" - - def __init__(self, number_concentration_pm10p0_raw): - self._number_concentration_pm10p0 = number_concentration_pm10p0_raw / 10.0 - - @property - def value(self): - return self._number_concentration_pm10p0 + return self._divided_by_10_int16 def __str__(self): return '{0:.2f}'.format(self.value) class SignalTemperature(AbstractSignal): - """Measured temperature in degrees celsius. The raw value is scaled appropriately.""" + """Measured temperature in degrees Celsius. The raw value is scaled appropriately.""" def __init__(self, temperature_raw): self._temperature = temperature_raw / 200.0 @@ -170,45 +78,3 @@ def value(self): def __str__(self): return '{0:.2f}'.format(self.value) - -class SignalVocIndex(AbstractSignal): - """Measured VOC Index ticks.""" - - def __init__(self, voc_index_raw): - self._voc_index = voc_index_raw / 10.0 - - @property - def value(self): - return self._voc_index - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalNoxIndex(AbstractSignal): - """Measured NOx Index ticks.""" - - def __init__(self, nox_index_raw): - self._nox_index = nox_index_raw / 10.0 - - @property - def value(self): - return self._nox_index - - def __str__(self): - return '{0:.2f}'.format(self.value) - - -class SignalCo2(AbstractSignal): - """Measured CO2 in ppm.""" - - def __init__(self, co2_raw): - self._co2 = co2_raw - - @property - def value(self): - return self._co2 - - def __str__(self): - return '{0}'.format(self.value) - diff --git a/sensirion_i2c_sen66/version.py b/sensirion_i2c_sen66/version.py index 8fd97df..2c6391c 100644 --- a/sensirion_i2c_sen66/version.py +++ b/sensirion_i2c_sen66/version.py @@ -1,5 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function +import importlib.metadata as metadata +from typing import Final -version = "1.2.0" +version: Final[str] = metadata.version("sensirion_i2c_sen66") diff --git a/setup.py b/setup.py index 86951ec..bfae448 100644 --- a/setup.py +++ b/setup.py @@ -1,70 +1,5 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from setuptools import setup -import os -import re - -from setuptools import setup, find_packages - -# Python versions this package is compatible with -python_requires = '>=3.6, <4' - -# Packages that this package imports. List everything apart from standard lib packages. -install_requires = [ - 'sensirion-i2c-driver>=1.0.0,<2.0', - 'sensirion-driver-adapters>=2.1.9,<3.0', - 'sensirion-driver-support-types>=1.1.0,<2.0', - 'sensirion-shdlc-sensorbridge>=0.1.0,<0.3.0' -] - -# Packages required for tests and docs -extras_require = { - 'test': [ - 'flake8~=3.7.8', - 'pytest~=6.2.5', - 'pytest-cov~=3.0.0', - ] -} - -# Read version number from version.py -version_line = open("sensirion_i2c_sen66/version.py", "rt").read() -result = re.search(r"^version = ['\"]([^'\"]*)['\"]", version_line, re.M) -if result: - version_string = result.group(1) -else: - raise RuntimeError("Unable to find version string") - -# Use README.rst and CHANGELOG.md as package description -root_path = os.path.dirname(__file__) -long_description = open(os.path.join(root_path, 'README.md')).read() - -setup( - name='sensirion_i2c_sen66', - version=version_string, - author='Sensirion', - author_email='info@sensirion.com', - description='I2C driver for the Sensirion SEN66 sensor family', - license='BSD', - keywords="""Sensirion SEN66 - I2C - SEN66""", - project_urls={ - "Documentation": "https://sensirion.github.io/python-i2c-sen66", - "Repository": "https://github.com/Sensirion/python-i2c-sen66", - "Changelog": "https://github.com/Sensirion/python-i2c-sen66/blob/master/CHANGELOG.md", - }, - packages=find_packages(exclude=['tests', 'tests.*']), - long_description=long_description, - long_description_content_type='text/markdown', - python_requires=python_requires, - install_requires=install_requires, - extras_require=extras_require, - classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.11', - 'Topic :: Software Development :: Libraries :: Python Modules' - ] -) +setup() \ No newline at end of file diff --git a/tests/test_sen66.py b/tests/test_sen66.py index 7a55cae..82894eb 100644 --- a/tests/test_sen66.py +++ b/tests/test_sen66.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# (c) Copyright 2025 Sensirion AG, Switzerland +# (c) Copyright 2026 Sensirion AG, Switzerland # # THIS FILE IS AUTOMATICALLY GENERATED! # -# Generator: sensirion-driver-generator 1.1.2 +# Generator: sensirion-driver-generator 1.7.0 # Product: sen66 -# Model-Version: 1.6.0 +# Model-Version: 1.7.1 # import pytest @@ -24,9 +24,9 @@ def sensor(channel_provider): yield Sen66Device(channel) -def test_perform_forced_co2_recalibration1(sensor): - correction = sensor.perform_forced_co2_recalibration(600) - print(f"correction: {correction}; " +def test_get_product_type1(sensor): + product_type = sensor.get_product_type() + print(f"product_type: {product_type}; " ) @@ -54,52 +54,34 @@ def test_set_temperature_offset_parameters1(sensor): sensor.set_temperature_offset_parameters(1, 10, 1, 0) -def test_set_voc_algorithm_tuning_parameters1(sensor): - sensor.set_voc_algorithm_tuning_parameters(100, 12, 12, 180, 50, 230) - - -def test_get_voc_algorithm_tuning_parameters1(sensor): - (index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, - gain_factor - ) = sensor.get_voc_algorithm_tuning_parameters() - print(f"index_offset: {index_offset}; " - f"learning_time_offset_hours: {learning_time_offset_hours}; " - f"learning_time_gain_hours: {learning_time_gain_hours}; " - f"gating_max_duration_minutes: {gating_max_duration_minutes}; " - f"std_initial: {std_initial}; " - f"gain_factor: {gain_factor}; " - ) +def test_set_temperature_acceleration_parameters1(sensor): + sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) -def test_set_nox_algorithm_tuning_parameters1(sensor): - sensor.set_nox_algorithm_tuning_parameters(1, 12, 12, 720, 50, 230) +def test_activate_sht_heater1(sensor): + sensor.activate_sht_heater() -def test_get_nox_algorithm_tuning_parameters1(sensor): - (index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, - gain_factor - ) = sensor.get_nox_algorithm_tuning_parameters() - print(f"index_offset: {index_offset}; " - f"learning_time_offset_hours: {learning_time_offset_hours}; " - f"learning_time_gain_hours: {learning_time_gain_hours}; " - f"gating_max_duration_minutes: {gating_max_duration_minutes}; " - f"std_initial: {std_initial}; " - f"gain_factor: {gain_factor}; " +def test_read_device_status1(sensor): + device_status = sensor.read_device_status() + print(f"device_status: {device_status}; " ) -def test_set_temperature_acceleration_parameters1(sensor): - sensor.set_temperature_acceleration_parameters(1, 1, 1, 1) +def test_read_and_clear_device_status1(sensor): + device_status = sensor.read_and_clear_device_status() + print(f"device_status: {device_status}; " + ) -def test_get_voc_algorithm_state1(sensor): - state = sensor.get_voc_algorithm_state() - print(f"state: {state}; " +def test_perform_forced_co2_recalibration1(sensor): + correction = sensor.perform_forced_co2_recalibration(600) + print(f"correction: {correction}; " ) -def test_set_co2_sensor_automatic_self_calibration1(sensor): - sensor.set_co2_sensor_automatic_self_calibration(0) +def test_perform_co2_sensor_factory_reset1(sensor): + sensor.perform_co2_sensor_factory_reset() def test_get_co2_sensor_automatic_self_calibration1(sensor): @@ -110,8 +92,18 @@ def test_get_co2_sensor_automatic_self_calibration1(sensor): ) -def test_set_ambient_pressure1(sensor): - sensor.set_ambient_pressure(1013) +def test_set_co2_sensor_automatic_self_calibration1(sensor): + sensor.set_co2_sensor_automatic_self_calibration(0) + + +def test_get_sensor_altitude1(sensor): + altitude = sensor.get_sensor_altitude() + print(f"altitude: {altitude}; " + ) + + +def test_set_sensor_altitude1(sensor): + sensor.set_sensor_altitude(0) def test_get_ambient_pressure1(sensor): @@ -120,29 +112,47 @@ def test_get_ambient_pressure1(sensor): ) -def test_set_sensor_altitude1(sensor): - sensor.set_sensor_altitude(0) +def test_set_ambient_pressure1(sensor): + sensor.set_ambient_pressure(1013) -def test_get_sensor_altitude1(sensor): - altitude = sensor.get_sensor_altitude() - print(f"altitude: {altitude}; " +def test_get_voc_algorithm_tuning_parameters1(sensor): + (index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_voc_algorithm_tuning_parameters() + print(f"index_offset: {index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " ) -def test_activate_sht_heater1(sensor): - sensor.activate_sht_heater() +def test_set_voc_algorithm_tuning_parameters1(sensor): + sensor.set_voc_algorithm_tuning_parameters(100, 12, 12, 180, 50, 230) -def test_read_device_status1(sensor): - device_status = sensor.read_device_status() - print(f"device_status: {device_status}; " +def test_get_nox_algorithm_tuning_parameters1(sensor): + (index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial, + gain_factor + ) = sensor.get_nox_algorithm_tuning_parameters() + print(f"index_offset: {index_offset}; " + f"learning_time_offset_hours: {learning_time_offset_hours}; " + f"learning_time_gain_hours: {learning_time_gain_hours}; " + f"gating_max_duration_minutes: {gating_max_duration_minutes}; " + f"std_initial: {std_initial}; " + f"gain_factor: {gain_factor}; " ) -def test_read_and_clear_device_status1(sensor): - device_status = sensor.read_and_clear_device_status() - print(f"device_status: {device_status}; " +def test_set_nox_algorithm_tuning_parameters1(sensor): + sensor.set_nox_algorithm_tuning_parameters(1, 12, 12, 720, 50, 230) + + +def test_get_voc_algorithm_state1(sensor): + state = sensor.get_voc_algorithm_state() + print(f"state: {state}; " ) @@ -206,8 +216,6 @@ def test_start_continuous_measurement1(sensor): f"raw_co2: {raw_co2}; " ) sensor.set_temperature_offset_parameters(1, 10, 1, 0) - state = sensor.get_voc_algorithm_state() - print(f"state: {state}; " - ) + sensor.set_ambient_pressure(1013) sensor.stop_measurement()