temperature-sensor-scanner 1.0.0__tar.gz

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,79 @@
1
+ name: Publish to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution
8
+ if: startsWith(github.ref, 'refs/tags/') # only build on tag pushes
9
+
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
14
+ with:
15
+ persist-credentials: false
16
+ - name: Set up Python
17
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
18
+ with:
19
+ python-version: "3.x"
20
+ - name: Install pypa/build
21
+ run: >-
22
+ python3 -m
23
+ pip install
24
+ build
25
+ --user
26
+ - name: Build a binary wheel and a source tarball
27
+ run: python3 -m build
28
+ - name: Store the distribution packages
29
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
30
+ with:
31
+ name: python-package-distributions
32
+ path: dist/
33
+
34
+ publish-to-pypi:
35
+ name: >-
36
+ Publish to PyPI
37
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
38
+ needs:
39
+ - build
40
+ runs-on: ubuntu-latest
41
+ environment:
42
+ name: pypi
43
+ url: https://pypi.org/p/temperature-sensor-scanner
44
+ permissions:
45
+ id-token: write # IMPORTANT: mandatory for trusted publishing
46
+
47
+ steps:
48
+ - name: Download all the dists
49
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
50
+ with:
51
+ name: python-package-distributions
52
+ path: dist/
53
+ - name: Publish distribution to PyPI
54
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
55
+
56
+ publish-to-testpypi:
57
+ name: Publish to TestPyPI
58
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
59
+ needs:
60
+ - build
61
+ runs-on: ubuntu-latest
62
+
63
+ environment:
64
+ name: testpypi
65
+ url: https://test.pypi.org/p/temperature-sensor-scanner
66
+
67
+ permissions:
68
+ id-token: write # IMPORTANT: mandatory for trusted publishing
69
+
70
+ steps:
71
+ - name: Download all the dists
72
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
73
+ with:
74
+ name: python-package-distributions
75
+ path: dist/
76
+ - name: Publish to TestPyPI
77
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
78
+ with:
79
+ repository-url: https://test.pypi.org/legacy/
@@ -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,43 @@
1
+ # temperature-sensor-scanner
2
+
3
+ A tool to scan nearby Bluetooth Low Energy thermometers on the Telink chipset, collect the data ane emit it into
4
+ InfluxDB.
5
+
6
+ ## Setup
7
+
8
+ * Create an influxdb user with a command like `influx user create --org [my-org] --name [my-username] --password [my-password]`
9
+ * Create an influxdb token with a command like `influx auth create --org [my-org] --user [my-username] --write-bucket [my-bucket-id]`
10
+ * Create a `config.yaml` in `~/.config/temperature-sensor-scanner/` based on the `config.example.yaml` file
11
+ * Install this tool with a command like `pip install temperature-sensor-scanner`
12
+
13
+ ## Usage
14
+
15
+ Run `temperature-sensor-scanner` which will scan for nearby sensors which are emitting temperature data in their Bluetooth
16
+ advertising string, collect that data, and emit it into InfluxDB.
17
+
18
+ # Notes
19
+
20
+ * https://github.com/pvvx/ATC_MiThermometer/
21
+ * https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html
22
+ * Launch in Chrome instead of Firefox
23
+ * Other tools that look interesting
24
+ * https://github.com/JsBergbau/MiTemperature2
25
+
26
+ ## Steps to provision a new sensor
27
+
28
+ * [Flash the firmware](https://github.com/pvvx/ATC_MiThermometer/?tab=readme-ov-file#flashing-or-updating-the-firmware-ota)
29
+ * Click `Connect`
30
+ * Select the `LYWSD03MMC` Bluetooth device in the menu
31
+ * Click `Do Activation`
32
+ * Choose `Custom Firmware ATC_v56.bin`
33
+ * Click `Start Flashing`
34
+ * Wait 42 seconds for it to show update done
35
+ * Identify ID from Bluetooth announced name and print a label to put on the sensor
36
+ * Quit Chrome and start again
37
+ * [Configure sensor](https://github.com/pvvx/ATC_MiThermometer/?tab=readme-ov-file#configuration)
38
+ * Set display to Farenheight
39
+ * Click `Set Time`
40
+ * Set advertising type to `ATC1441`
41
+ * Set advertising interval to `10000` ( https://github.com/pvvx/ATC_MiThermometer/issues/23#issuecomment-766898945 )
42
+ * Set Measure interval: to 10
43
+ * Click `Send Config`
@@ -0,0 +1,20 @@
1
+ sensors:
2
+ ABCDEF123456:
3
+ bindkey: 0123456789abcdef0123456789abcdef
4
+ domain: outdoors
5
+ location: John's room, window next to closet
6
+ name: ATC_123456
7
+ mac: ABCDEF1234567890
8
+
9
+ BCDEF1234567:
10
+ bindkey: 123456789abcdef0123456789abcdef0
11
+ domain: indoors
12
+ location: Alice's room, on bookshelf
13
+ name: ATC_234567
14
+ mac: BCDEF12345678901
15
+
16
+ influxdb:
17
+ bucket: home
18
+ org: my-org
19
+ token: abcdefghijklmnopqrstuvwxyz-0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789-abcdefghij==
20
+ url: https://influxdb.example.com
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "temperature-sensor-scanner"
7
+ version = "1.0.0"
8
+ description = "A tool to collect data from temperature sensors and emit it into InfluxDB"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "GPL-3.0-or-later" }
12
+ authors = [
13
+ { name = "Gene Wood", email = "gene_wood@cementhorizon.com" }
14
+ ]
15
+ dependencies = [
16
+ "bleak",
17
+ "atc-mi-interface",
18
+ "influxdb-client",
19
+ "platformdirs",
20
+ "PyYAML"
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/gene1wood/temperature-sensor-scanner"
25
+ Repository = "https://github.com/gene1wood/temperature-sensor-scanner"
26
+
27
+ # CLI entry point
28
+ [project.scripts]
29
+ temperature-sensor-scanner = "temperature_sensor_scanner:main"
30
+
31
+ # Tell hatchling where your package lives
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["temperature_sensor_scanner"]
34
+
35
+ [tool.black]
36
+ preview = true
37
+ unstable = true
@@ -0,0 +1,5 @@
1
+ bleak
2
+ atc-mi-interface
3
+ influxdb-client
4
+ platformdirs
5
+ PyYAML
@@ -0,0 +1,2 @@
1
+ [flake8]
2
+ max-line-length = 92
@@ -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()