├── .gitignore ├── requirements.txt └── co2mon.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | hidraw-pure 2 | graphitesend 3 | flask -------------------------------------------------------------------------------- /co2mon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import os 4 | from typing import BinaryIO, Union 5 | 6 | import hidraw 7 | 8 | _cstate = b"Htemp99e" 9 | _shuffle = (2, 4, 0, 7, 1, 6, 5, 3) 10 | _ctmp = bytes([((_cstate[i] >> 4) | (_cstate[i] << 4)) & 0xff for i in range(8)]) 11 | 12 | 13 | # https://github.com/poempelfox/co2sensorsw/blob/master/co2sensord.c 14 | def _decrypt(buf: bytes, key: bytes) -> bytes: 15 | assert len(buf) == 8 16 | assert len(key) == 8 17 | phase1 = bytearray(8) 18 | for j in range(8): 19 | phase1[_shuffle[j]] = buf[j] 20 | phase2 = [phase1[i] ^ key[i] for i in range(8)] 21 | phase3 = [(phase2[i] >> 3) | ((phase2[(i + 7) % 8] << 5) & 0xff) for i in range(8)] 22 | out = [(0x100 + phase3[i] - _ctmp[i]) & 0xff for i in range(8)] 23 | return bytes(out) 24 | 25 | 26 | def init_sensor(dev: hidraw.HIDRaw, key: bytes): 27 | assert len(key) == 8 28 | dev.sendFeatureReport(key) 29 | 30 | 31 | def read_sensor(dev: BinaryIO, key: bytes): 32 | buf = dev.read(8) 33 | return _parse(_decrypt(buf, key)) 34 | 35 | 36 | def generate_key() -> bytes: 37 | return os.urandom(8) 38 | 39 | 40 | class Temperature: 41 | def __init__(self, raw: int): 42 | self.raw = raw 43 | 44 | def kelvin(self) -> float: 45 | return self.raw / 16.0 46 | 47 | def celsius(self) -> float: 48 | return self.kelvin() - 273.15 49 | 50 | def __repr__(self): 51 | return "Temperature(%s = %s°C)" % (self.raw, self.celsius()) 52 | 53 | 54 | class CO2: 55 | def __init__(self, raw: int): 56 | self.raw = raw 57 | 58 | def ppm(self) -> int: 59 | return self.raw 60 | 61 | def __repr__(self): 62 | return "CO2(%s ppm)" % self.ppm() 63 | 64 | 65 | class Unknown: 66 | # noinspection PyShadowingBuiltins 67 | def __init__(self, type: int, raw: int): 68 | self.type = type 69 | self.raw = raw 70 | 71 | def __repr__(self): 72 | return "Unknown(type=0x%x, %s)" % (self.type, self.raw) 73 | 74 | 75 | class FormatError(IOError): 76 | def __init__(self, msg): 77 | super(FormatError, self).__init__(msg) 78 | 79 | 80 | def _parse(decrypted: bytes) -> Union[CO2, Temperature, Unknown]: 81 | expected_sum = sum(decrypted[0:3]) & 0xff 82 | if expected_sum != decrypted[3]: 83 | raise FormatError("Checksum mismatch: '%s'" % decrypted) 84 | if decrypted[4:] != b"\x0d\x00\x00\x00": 85 | raise FormatError("Padding mismatch: '%s'" % decrypted) 86 | raw = (decrypted[1] << 8) | decrypted[2] 87 | if decrypted[0] == 0x50: 88 | return CO2(raw) 89 | elif decrypted[0] == 0x42: 90 | return Temperature(raw) 91 | else: 92 | return Unknown(decrypted[0], raw) 93 | 94 | 95 | def _main(): 96 | import graphitesend 97 | import sys 98 | import argparse 99 | 100 | parser = argparse.ArgumentParser() 101 | parser.add_argument("file") 102 | parser.add_argument("--graphite-server") 103 | parser.add_argument("--graphite-id", default='0') 104 | parser.add_argument("--http-host", default='127.0.0.1') 105 | parser.add_argument("--http-port", type=int) 106 | args = parser.parse_args() 107 | 108 | graphite = args.graphite_server is not None 109 | if graphite: 110 | graphitesend.init(graphite_server=args.graphite_server, prefix="co2mon." + args.graphite_id, 111 | system_name="") 112 | 113 | state = {} 114 | 115 | if args.http_port: 116 | import flask 117 | import threading 118 | app = flask.Flask("co2mon") 119 | 120 | @app.route("/") 121 | def show_state(): 122 | return flask.jsonify(state) 123 | 124 | def run(): 125 | app.run(host=args.http_host, port=args.http_port) 126 | 127 | threading.Thread(target=run, daemon=True).start() 128 | 129 | with open(sys.argv[1], "r+b") as dev: 130 | key = generate_key() 131 | hid = hidraw.HIDRaw(dev.fileno()) 132 | init_sensor(hid, key) 133 | while True: 134 | data = read_sensor(dev, key) 135 | if type(data) == CO2: 136 | if graphite: 137 | graphitesend.send("co2", data.ppm()) 138 | state["co2"] = data.ppm() 139 | elif type(data) == Temperature: 140 | if graphite: 141 | graphitesend.send("temperature", data.celsius()) 142 | state["temperature"] = data.celsius() 143 | 144 | 145 | if __name__ == '__main__': 146 | _main() 147 | --------------------------------------------------------------------------------