temperature-sensor-scanner 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import asyncio
4
+ from bleak import BleakScanner # pip install bleak
5
+ from functools import partial
6
+ from atc_mi_interface import (
7
+ general_format,
8
+ atc_mi_advertising_format,
9
+ ) # pip install atc-mi-interface
10
+
11
+ # atc-mi-interface
12
+ # https://github.com/pvvx/ATC_MiThermometer/tree/master/python-interface#decoding-and-encoding
13
+ import influxdb_client # pip install influxdb-client
14
+ from influxdb_client.client.write_api import SYNCHRONOUS
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ import yaml # pip install PyYAML
20
+ from platformdirs import user_config_dir # pip install platformdirs
21
+
22
+ APP_NAME = "temperature-sensor-scanner"
23
+ APP_AUTHOR = "Cementhorizon"
24
+
25
+
26
+ def get_config_path() -> Path:
27
+ """
28
+ Returns the full path to config.yaml in the user config directory.
29
+ Example:
30
+ Linux: ~/.config/APP_NAME/config.yaml
31
+ macOS: ~/Library/Application Support/APP_NAME/config.yaml
32
+ Windows: C:\\Users\\<user>\\AppData\\Local\\APP_AUTHOR\\APP_NAME\\config.yaml
33
+ """
34
+ config_dir = Path(user_config_dir(APP_NAME, APP_AUTHOR))
35
+ return config_dir / "config.yaml"
36
+
37
+
38
+ def load_config(path: Path) -> dict:
39
+ """
40
+ Loads YAML config.
41
+ """
42
+ if not path.exists():
43
+ print("Missing parsing config file", file=sys.stderr)
44
+ sys.exit(1)
45
+
46
+ try:
47
+ with path.open("r") as f:
48
+ return yaml.safe_load(f) or {}
49
+ except yaml.YAMLError as e:
50
+ print(f"Error parsing config file: {e}", file=sys.stderr)
51
+ sys.exit(1)
52
+
53
+
54
+ def gather_data(config: dict) -> list:
55
+ scan_results = asyncio.run(ble_coro(config))
56
+
57
+ # name = atc_mi_data.atc1441_format[0].MAC.replace(":", "")[-6:]
58
+ # print(f"ATC_{name} : {temperature:.1f}")
59
+
60
+ points = []
61
+ macs_seen = set()
62
+ for scan_result in scan_results:
63
+ if scan_result["mac"] in macs_seen:
64
+ continue
65
+ else:
66
+ macs_seen.add(scan_result["mac"])
67
+ sensor = config["sensors"][scan_result["mac"]]
68
+ points.append(
69
+ influxdb_client.Point("environment")
70
+ .tag("location", sensor["location"])
71
+ .tag("domain", sensor["domain"])
72
+ .tag("mac", scan_result["mac"])
73
+ .field("temperature", scan_result["temperature"])
74
+ )
75
+ return points
76
+
77
+
78
+ def emit_data(config: dict, points: list) -> None:
79
+ bucket = config["influxdb"]["bucket"]
80
+ org = config["influxdb"]["org"]
81
+ token = config["influxdb"]["token"]
82
+ url = config["influxdb"]["url"]
83
+
84
+ client = influxdb_client.InfluxDBClient(url=url, token=token, org=org)
85
+
86
+ write_api = client.write_api(write_options=SYNCHRONOUS)
87
+ write_api.write(bucket=bucket, org=org, record=points)
88
+
89
+
90
+ async def ble_coro(config):
91
+ count = [0]
92
+ stop_event = asyncio.Event()
93
+ results = []
94
+
95
+ def detection_callback(count, results, device, advertisement_data):
96
+ format_label, adv_data = atc_mi_advertising_format(advertisement_data)
97
+ if not adv_data:
98
+ return
99
+ mac_address = bytes.fromhex(device.address.replace(":", ""))
100
+ bindkey = config["sensors"][device.address.replace(":", "")]["bindkey"]
101
+
102
+ atc_mi_data = general_format.parse(
103
+ adv_data,
104
+ mac_address=mac_address,
105
+ bindkey=None if bindkey is None else bytes.fromhex(bindkey),
106
+ )
107
+ name = atc_mi_data.atc1441_format[0].MAC.replace(":", "")[-6:]
108
+ if atc_mi_data.atc1441_format[0].temperature_unit == "°C":
109
+ temperature = (atc_mi_data.atc1441_format[0].temperature * 1.8) + 32
110
+ else:
111
+ temperature = atc_mi_data.atc1441_format[0].temperature
112
+ print(f"ATC_{name} : {temperature:.1f}")
113
+
114
+ results.append({
115
+ "mac": atc_mi_data.atc1441_format[0].MAC.replace(":", ""),
116
+ "temperature": temperature,
117
+ })
118
+
119
+ count[0] += 1
120
+ if count[0] == 8:
121
+ stop_event.set()
122
+
123
+ async with BleakScanner(
124
+ detection_callback=partial(detection_callback, count, results)
125
+ ) as scanner:
126
+ await stop_event.wait()
127
+ return results
128
+
129
+
130
+ def main():
131
+ config_path = get_config_path()
132
+ config = load_config(config_path)
133
+ points = gather_data(config)
134
+ emit_data(config, points)
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: temperature-sensor-scanner
3
+ Version: 1.0.0
4
+ Summary: A tool to collect data from temperature sensors and emit it into InfluxDB
5
+ Project-URL: Homepage, https://github.com/gene1wood/temperature-sensor-scanner
6
+ Project-URL: Repository, https://github.com/gene1wood/temperature-sensor-scanner
7
+ Author-email: Gene Wood <gene_wood@cementhorizon.com>
8
+ License: GPL-3.0-or-later
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: atc-mi-interface
11
+ Requires-Dist: bleak
12
+ Requires-Dist: influxdb-client
13
+ Requires-Dist: platformdirs
14
+ Requires-Dist: pyyaml
15
+ Description-Content-Type: text/markdown
16
+
17
+ # temperature-sensor-scanner
18
+
19
+ A tool to scan nearby Bluetooth Low Energy thermometers on the Telink chipset, collect the data ane emit it into
20
+ InfluxDB.
21
+
22
+ ## Setup
23
+
24
+ * Create an influxdb user with a command like `influx user create --org [my-org] --name [my-username] --password [my-password]`
25
+ * Create an influxdb token with a command like `influx auth create --org [my-org] --user [my-username] --write-bucket [my-bucket-id]`
26
+ * Create a `config.yaml` in `~/.config/temperature-sensor-scanner/` based on the `config.example.yaml` file
27
+ * Install this tool with a command like `pip install temperature-sensor-scanner`
28
+
29
+ ## Usage
30
+
31
+ Run `temperature-sensor-scanner` which will scan for nearby sensors which are emitting temperature data in their Bluetooth
32
+ advertising string, collect that data, and emit it into InfluxDB.
33
+
34
+ # Notes
35
+
36
+ * https://github.com/pvvx/ATC_MiThermometer/
37
+ * https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html
38
+ * Launch in Chrome instead of Firefox
39
+ * Other tools that look interesting
40
+ * https://github.com/JsBergbau/MiTemperature2
41
+
42
+ ## Steps to provision a new sensor
43
+
44
+ * [Flash the firmware](https://github.com/pvvx/ATC_MiThermometer/?tab=readme-ov-file#flashing-or-updating-the-firmware-ota)
45
+ * Click `Connect`
46
+ * Select the `LYWSD03MMC` Bluetooth device in the menu
47
+ * Click `Do Activation`
48
+ * Choose `Custom Firmware ATC_v56.bin`
49
+ * Click `Start Flashing`
50
+ * Wait 42 seconds for it to show update done
51
+ * Identify ID from Bluetooth announced name and print a label to put on the sensor
52
+ * Quit Chrome and start again
53
+ * [Configure sensor](https://github.com/pvvx/ATC_MiThermometer/?tab=readme-ov-file#configuration)
54
+ * Set display to Farenheight
55
+ * Click `Set Time`
56
+ * Set advertising type to `ATC1441`
57
+ * Set advertising interval to `10000` ( https://github.com/pvvx/ATC_MiThermometer/issues/23#issuecomment-766898945 )
58
+ * Set Measure interval: to 10
59
+ * Click `Send Config`
@@ -0,0 +1,5 @@
1
+ temperature_sensor_scanner/__init__.py,sha256=uf2cnbKn_pwDp81FuNfJho0TU82m6UsOG3cnwTlXmZQ,4225
2
+ temperature_sensor_scanner-1.0.0.dist-info/METADATA,sha256=CvSSYHNKCluHKNxa5z3k1dcLigBp-XS3Fx81OjlLJBk,2505
3
+ temperature_sensor_scanner-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
4
+ temperature_sensor_scanner-1.0.0.dist-info/entry_points.txt,sha256=-RABphDkoz7DtXP0eymic_gO7Mt8wN6VW2fmHvXL7FU,79
5
+ temperature_sensor_scanner-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ temperature-sensor-scanner = temperature_sensor_scanner:main