├── pmsensor ├── __init__.py ├── co2sensor.py └── serial_pm.py ├── documents ├── a3.pdf ├── ZH03B.pdf ├── SDS011.pdf └── SDS021.pdf ├── dist ├── pmsensor-0.1.tar.gz ├── pmsensor-0.2.tar.gz └── pmsensor-0.1.1.tar.gz ├── .gitignore ├── test.sh ├── co2_demo.py ├── README.rst ├── SECURITY.md ├── setup.py ├── pmsensor_demo.py ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── LICENSE └── CODE_OF_CONDUCT.md /pmsensor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documents/a3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/documents/a3.pdf -------------------------------------------------------------------------------- /documents/ZH03B.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/documents/ZH03B.pdf -------------------------------------------------------------------------------- /documents/SDS011.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/documents/SDS011.pdf -------------------------------------------------------------------------------- /documents/SDS021.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/documents/SDS021.pdf -------------------------------------------------------------------------------- /dist/pmsensor-0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/dist/pmsensor-0.1.tar.gz -------------------------------------------------------------------------------- /dist/pmsensor-0.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/dist/pmsensor-0.2.tar.gz -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .pydevproject 3 | __pycache__ 4 | pmsensor.egg-info 5 | .settings 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /dist/pmsensor-0.1.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bencorrado/pmsensor/HEAD/dist/pmsensor-0.1.1.tar.gz -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd `dirname $0` 3 | find . -name __pycache__ | xargs rm -rf 4 | pylint pmsensor/*.py 5 | PYTHONPATH=. py.test 6 | 7 | -------------------------------------------------------------------------------- /co2_demo.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Aug 19, 2016 3 | 4 | @author: matuschd 5 | ''' 6 | 7 | from pmsensor import co2sensor 8 | 9 | if __name__ == '__main__': 10 | ppm = co2sensor.read_mh_z19("/dev/tty.SLAB_USBtoUART") 11 | print("CO2 concentration is {} ppm".format(ppm)) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | psensor - Library for particulate matter sensors 2 | ================================================ 3 | 4 | This library lets you read sensor data from serial-connected particulate matter sensors. Currently it supports the following sensors: 5 | 6 | - OneAir A3 7 | - Nova Fitness SDS021 8 | - Nova Fitness SDL607 9 | - Plantower PMS1003 10 | - Plantower PMS2003 11 | - Plantower PMS3003 12 | - Plantower PMS5003 13 | - Plantower PMS7003 14 | - Winsen ZH03B 15 | 16 | It also supports the following CO2-Sensor: 17 | 18 | - MH-Z19 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of pmsensors are currently being supported 6 | with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 0.5 | :white_check_mark: | 11 | | 0.4 | :white_check_mark: | 12 | | < 0.4 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a vunerablity please e-mail bencorrado+security@gmail.com. 17 | Please allow two weeks for a response before creating an issue or 18 | publishing it publicly before an update can be release. 19 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='pmsensor', 4 | version='0.5', 5 | description='Library to read data from environment ensors', 6 | url='https://github.com/open-homeautomation/pmsensor', 7 | author='Daniel Matuschek', 8 | author_email='daniel@matuschek.net', 9 | license='MIT', 10 | classifiers=[ 11 | 'Development Status :: 3 - Alpha', 12 | 'Intended Audience :: Developers', 13 | 'Topic :: System :: Hardware :: Hardware Drivers', 14 | 'License :: OSI Approved :: MIT License', 15 | 'Programming Language :: Python :: 3', 16 | 'Programming Language :: Python :: 3.4', 17 | 'Programming Language :: Python :: 3.5' 18 | ], 19 | packages=find_packages(), 20 | install_requires=['pyserial>=3'], 21 | keywords='serial pm2.5 pm1.0 pm10 co2', 22 | zip_safe=False) 23 | -------------------------------------------------------------------------------- /pmsensor_demo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from pmsensor import serial_pm as pm 4 | 5 | 6 | def main(): 7 | logging.basicConfig(level=logging.INFO) 8 | sensors = [] 9 | # sensors.append(pm.PMDataCollector("/dev/tty.wchusbserial144740", 10 | # pm.SUPPORTED_SENSORS["novafitness,sds011"])) 11 | sensors.append(pm.PMDataCollector("/dev/tty.SLAB_USBtoUART", 12 | pm.SUPPORTED_SENSORS["oneair,s3"])) 13 | # sensors.append(pm.PMDataCollector("/dev/tty.SLAB_USBtoUART4", 14 | # pm.SUPPORTED_SENSORS["plantower,pms7003"])) 15 | # sensors.append(pm.PMDataCollector("/dev/tty.SLAB_USBtoUART", 16 | # pm.SUPPORTED_SENSORS["winsen,zh03b"])) 17 | 18 | for s in sensors: 19 | print(s.supported_values()) 20 | 21 | while True: 22 | for s in sensors: 23 | print(s.read_data()) 24 | time.sleep(3) 25 | 26 | if __name__ == '__main__': 27 | main() 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Environment (please complete the following information):** 27 | - OS: [e.g. haos, Raspbian ] 28 | - Version [e.g. 22] 29 | - Hardware [e.g. RPi 4B 8GB] 30 | 31 | **PM Sensor (please complete the following information):** 32 | - Device: [e.g. Plantower PMS7003] 33 | - Serial chipset: [e.g. Prolific PL2303] 34 | - USB VID/PID [e.g. VID: 0x4ce, PID: 0x4ce ] 35 | - Driver [e.g. PL2303G v5.1.3.2] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ben Corrado 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pmsensor/co2sensor.py: -------------------------------------------------------------------------------- 1 | """" 2 | Read data from CO2 sensor 3 | """ 4 | 5 | import time 6 | import logging 7 | 8 | import serial 9 | 10 | MHZ19_SIZE = 9 11 | MZH19_READ = [0xff, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79] 12 | MZH19_RESET = [0xff, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78] 13 | 14 | def reset_mh_z19(serial_device): 15 | """reset to zero""" 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | ser = serial.Serial(port=serial_device, 20 | baudrate=9600, 21 | parity=serial.PARITY_NONE, 22 | stopbits=serial.STOPBITS_ONE, 23 | bytesize=serial.EIGHTBITS) 24 | 25 | ser.write(MZH19_RESET) 26 | 27 | return None 28 | 29 | def read_mh_z19(serial_device): 30 | """ Read the CO2 PPM concenration from a MH-Z19 sensor""" 31 | 32 | result = read_mh_z19_with_temperature(serial_device) 33 | if result is None: 34 | return None 35 | ppm, temp = result 36 | return ppm 37 | 38 | 39 | def read_mh_z19_with_temperature(serial_device): 40 | """ Read the CO2 PPM concenration and temperature from a MH-Z19 sensor""" 41 | 42 | logger = logging.getLogger(__name__) 43 | 44 | ser = serial.Serial(port=serial_device, 45 | baudrate=9600, 46 | parity=serial.PARITY_NONE, 47 | stopbits=serial.STOPBITS_ONE, 48 | bytesize=serial.EIGHTBITS) 49 | 50 | sbuf = bytearray() 51 | starttime = time.time() 52 | finished = False 53 | timeout = 2 54 | res = None 55 | ser.write(MZH19_READ) 56 | while not finished: 57 | mytime = time.time() 58 | if mytime - starttime > timeout: 59 | logger.error("read timeout after %s seconds, read %s bytes", 60 | timeout, len(sbuf)) 61 | return None 62 | 63 | if ser.inWaiting() > 0: 64 | sbuf += ser.read(1) 65 | 66 | if len(sbuf) == MHZ19_SIZE: 67 | logger.debug("Finished reading data %s", sbuf) 68 | 69 | received_checksum = sbuf[-1] 70 | logger.debug("received_checksum: %s", received_checksum) 71 | # checksum: (NOT (Byte1+Byte1+Byte2+Byte3+Byte5+Byte6+Byte7)) + 1 72 | calculated_checksum = (~sum(bytearray(sbuf[1:8])) & 0xFF) + 1 73 | logger.debug("calculated_checksum: %s", calculated_checksum) 74 | if sbuf[0] != 0xFF or received_checksum != calculated_checksum: 75 | logger.error('bad checksum for data: %s received: %s calculated: %s', 76 | sbuf, received_checksum, calculated_checksum) 77 | return None 78 | 79 | res = (sbuf[2]*256 + sbuf[3], sbuf[4] - 40) 80 | finished = True 81 | 82 | else: 83 | time.sleep(.1) 84 | logger.debug("Serial waiting for data, buffer length=%s", 85 | len(sbuf)) 86 | 87 | return res 88 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement using a Github 63 | issue. All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /pmsensor/serial_pm.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reading data from particulate matter sensors with a serial interface. 3 | """ 4 | import time 5 | import threading 6 | import logging 7 | 8 | import serial 9 | 10 | STARTBLOCK = "SB" 11 | RECORD_LENGTH = "RL" 12 | # Ofsets of the PM data (always 2 byte) 13 | PM_1_0 = "1" 14 | PM_2_5 = "2.5" 15 | PM_10 = "10" 16 | BAUD_RATE = "BAUD" 17 | BYTE_ORDER = "BO", 18 | LSB = "lsb" 19 | MSB = "msb" 20 | DTR_ON = "DTR" 21 | DTR_OFF = "NOT_DTR" 22 | MULTIPLIER = "MP" 23 | TIMEOUT = "TO" 24 | 25 | PMVALS = [PM_1_0, PM_2_5, PM_10] 26 | 27 | 28 | ONEAIR_S3 = { 29 | "name": "OneAir S3", 30 | STARTBLOCK: bytes([0x32, 0x3d, 0x00, 0x1c]), 31 | RECORD_LENGTH: 32, 32 | PM_1_0: 6, 33 | PM_2_5: 8, 34 | PM_10: 10, 35 | BAUD_RATE: 9600, 36 | BYTE_ORDER: MSB, 37 | MULTIPLIER: 1, 38 | TIMEOUT: 2 39 | } 40 | 41 | NOVA_SDS = { 42 | "name": "Nova SDS0x1", 43 | STARTBLOCK: bytes([0xaa, 0xc0]), 44 | RECORD_LENGTH: 10, 45 | PM_1_0: None, 46 | PM_2_5: 2, 47 | PM_10: 4, 48 | BAUD_RATE: 9600, 49 | BYTE_ORDER: LSB, 50 | MULTIPLIER: 0.1, 51 | TIMEOUT: 2 52 | } 53 | 54 | # Data from 55 | # https://github.com/cezbloch/smog/blob/master/SmogMeters.py 56 | # PCB internal marking "SDL307" 57 | NOVA_SDL = { 58 | 'name': 'Nova SDLx07', 59 | STARTBLOCK: bytes([0xaa, 0xa5]), 60 | RECORD_LENGTH: 19, 61 | PM_1_0: None, 62 | PM_2_5: 7, 63 | PM_10: 11, 64 | BAUD_RATE: 9600, 65 | BYTE_ORDER: LSB, 66 | MULTIPLIER: 0.1, 67 | TIMEOUT: 2 68 | } 69 | 70 | # Data from 71 | # https://github.com/avaldebe/AQmon/blob/master/lua_modules/pms3003.lua 72 | PLANTOWER1 = { 73 | "name": "Plantower PMS1003/5003,7003", 74 | STARTBLOCK: bytes([0x42, 0x4d, 0x00, 0x1c]), 75 | RECORD_LENGTH: 32, 76 | PM_1_0: 4, 77 | PM_2_5: 6, 78 | PM_10: 8, 79 | BAUD_RATE: 9600, 80 | BYTE_ORDER: MSB, 81 | MULTIPLIER: 1, 82 | TIMEOUT: 2 83 | } 84 | 85 | PLANTOWER2 = { 86 | "name": "Plantower PMS2003/3003", 87 | STARTBLOCK: bytes([0x42, 0x4d, 0x00, 0x14]), 88 | RECORD_LENGTH: 24, 89 | PM_1_0: 4, 90 | PM_2_5: 6, 91 | PM_10: 8, 92 | BAUD_RATE: 9600, 93 | BYTE_ORDER: MSB, 94 | MULTIPLIER: 1, 95 | TIMEOUT: 2 96 | } 97 | 98 | # Data from 99 | # https://www.winsen-sensor.com/d/files/ZH03B.pdf 100 | WINSEN = { 101 | "name": "Winsen ZH03B", 102 | STARTBLOCK: bytes([0x42, 0x4d, 0x00, 0x14]), 103 | RECORD_LENGTH: 24, 104 | PM_1_0: 10, 105 | PM_2_5: 12, 106 | PM_10: 14, 107 | BAUD_RATE: 9600, 108 | BYTE_ORDER: MSB, 109 | MULTIPLIER: 1, 110 | TIMEOUT: 2 111 | } 112 | 113 | SUPPORTED_SENSORS = { 114 | "oneair,s3": ONEAIR_S3, 115 | "novafitness,sds021": NOVA_SDS, 116 | "novafitness,sds011": NOVA_SDS, 117 | "novafitness,sdl607": NOVA_SDL, 118 | "plantower,pms1003": PLANTOWER1, 119 | "plantower,pms5003": PLANTOWER1, 120 | "plantower,pms7003": PLANTOWER1, 121 | "plantower,pms2003": PLANTOWER2, 122 | "plantower,pms3003": PLANTOWER2, 123 | "winsen,zh03b": WINSEN, 124 | } 125 | 126 | 127 | LOGGER = logging.getLogger(__name__) 128 | 129 | 130 | class PMDataCollector(): 131 | """Controls the serial interface and reads data from the sensor.""" 132 | 133 | # pylint: disable=too-many-instance-attributes 134 | def __init__(self, 135 | serialdevice, 136 | configuration, 137 | power_control=DTR_ON, 138 | scan_interval=0): 139 | """Initialize the data collector based on the given parameters.""" 140 | 141 | self.record_length = configuration[RECORD_LENGTH] 142 | self.start_sequence = configuration[STARTBLOCK] 143 | self.byte_order = configuration[BYTE_ORDER] 144 | self.multiplier = configuration[MULTIPLIER] 145 | self.timeout = configuration[TIMEOUT] 146 | self.scan_interval = scan_interval 147 | self.listeners = [] 148 | self.power_control = power_control 149 | self.sensordata = {} 150 | self.config = configuration 151 | self.data = None 152 | self.last_poll = None 153 | self.start_func = None 154 | self.stop_func = None 155 | 156 | self.ser = serial.Serial(port=serialdevice, 157 | baudrate=configuration[BAUD_RATE], 158 | parity=serial.PARITY_NONE, 159 | stopbits=serial.STOPBITS_ONE, 160 | bytesize=serial.EIGHTBITS, 161 | timeout=0.1) 162 | 163 | # Update date in using a background thread 164 | if self.scan_interval > 0: 165 | thread = threading.Thread(target=self.refresh, args=()) 166 | thread.daemon = True 167 | thread.start() 168 | 169 | def refresh(self): 170 | """Background refreshing thread.""" 171 | while True: 172 | self.read_data() 173 | time.sleep(self.scan_interval) 174 | 175 | # pylint: disable=too-many-branches 176 | def read_data(self): 177 | """Read data from serial interface and return it as a dictionary. 178 | 179 | There is some caching implemented the sensor won't be polled twice 180 | within a 15 second interval. If data is requested within 15 seconds 181 | after it has been read, the data from the last read_data operation will 182 | be returned again 183 | """ 184 | 185 | mytime = time.time() 186 | if (self.last_poll is not None) and \ 187 | (mytime - self.last_poll) <= 15: 188 | return self._data 189 | 190 | # Start function that can do several things (e.g. turning the 191 | # sensor on) 192 | if self.start_func: 193 | self.start_func(self.ser) 194 | 195 | res = None 196 | finished = False 197 | sbuf = bytearray() 198 | starttime = time.time() 199 | checkCode = int(0); 200 | expectedCheckCode = int() 201 | #it is necessary to reset input buffer because data is cotinously received by the system and placed in the device buffer when serial is open. 202 | #But "Home Assistant" code read it only from time to time so the data we read here would be placed in the past. 203 | #Better is to clean the buffer and read new data from "present" time. 204 | self.ser.reset_input_buffer() 205 | while not finished: 206 | mytime = time.time() 207 | if mytime - starttime > self.timeout: 208 | LOGGER.error("read timeout after %s seconds, read %s bytes", 209 | self.timeout, len(sbuf)) 210 | return {} 211 | 212 | if self.ser.inWaiting() > 0: 213 | sbuf += self.ser.read(1) 214 | if len(sbuf) == len(self.start_sequence): 215 | if sbuf == self.start_sequence: 216 | LOGGER.debug("Found start sequence %s", 217 | self.start_sequence) 218 | else: 219 | LOGGER.debug("Start sequence not yet found") 220 | # Remove first character 221 | sbuf = sbuf[1:] 222 | 223 | if len(sbuf) == self.record_length: 224 | #Check the control sum if it is known how to do it 225 | if self.config == PLANTOWER1: 226 | for c in sbuf[0:(self.record_length-2)]: 227 | checkCode += c 228 | expectedCheckCode = sbuf[30]*256 + sbuf[31] 229 | if checkCode != expectedCheckCode: 230 | #because of data inconsistency clean the buffer 231 | LOGGER.error("PM sensor data sum error %d, expected %d", checkCode, expectedCheckCode) 232 | sbuf = [] 233 | checkCode = 0 234 | continue 235 | 236 | #if it is ok then send it for interpretation 237 | res = self.parse_buffer(sbuf) 238 | LOGGER.debug("Finished reading data %s", sbuf) 239 | finished = True 240 | 241 | else: 242 | time.sleep(.5) 243 | LOGGER.debug("Serial waiting for data, buffer length=%s", 244 | len(sbuf)) 245 | 246 | if self.stop_func: 247 | self.stop_func(self.ser) 248 | 249 | self._data = res 250 | self.last_poll = time.time() 251 | return res 252 | 253 | def parse_buffer(self, sbuf): 254 | """Parse the buffer and return the PM values.""" 255 | res = {} 256 | for pmname in PMVALS: 257 | offset = self.config[pmname] 258 | if offset is not None: 259 | if self.byte_order == MSB: 260 | res[pmname] = sbuf[offset] * \ 261 | 256 + sbuf[offset + 1] 262 | else: 263 | res[pmname] = sbuf[offset + 1] * \ 264 | 256 + sbuf[offset] 265 | 266 | res[pmname] = round(res[pmname] * self.multiplier, 1) 267 | 268 | return res 269 | 270 | def supported_values(self): 271 | res = [] 272 | for pmname in PMVALS: 273 | offset = self.config[pmname] 274 | if offset is not None: 275 | res.append(pmname) 276 | return res 277 | --------------------------------------------------------------------------------