Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc413b0336 | |||
| cc022e4bf8 | |||
| d607a1ed29 | |||
| 2768616be4 | |||
| 31c7f870e0 | |||
| 41654932e5 | |||
| b33d3d4cdb | |||
| 2d2e31ec29 | |||
| 8b5852c012 | |||
| 7dbabc902b | |||
| 2b4faca80d | |||
| d28ff065f5 |
@@ -18,7 +18,6 @@ dist/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|||||||
@@ -1,18 +1,61 @@
|
|||||||
# pico-thermo
|
# pico-thermo
|
||||||
|
|
||||||
MicroPython app to read data from BME280 sensor and send it to Home Assistant
|

|
||||||
|

|
||||||
|
|
||||||
## Running
|
A [MicroPython](https://micropython.org/) app to read data from BME280 sensor and send it to Home Assistant.
|
||||||
|
|
||||||
Flash your Raspberry Pi Pico with MicroPython.
|
## Hardware
|
||||||
|
|
||||||
Copy `config.example.py` to `config.py` and edit the settings to match your environment.
|
The following hardware is what I used for a deployment:
|
||||||
|
|
||||||
Copy `main.py` and `config.py` to the root of the filesystem on the Pico.
|
* Raspberry Pi Pico 2 W: https://www.amazon.com/dp/B0DRJXPPWL
|
||||||
|
* The **W** is VERY important, as without networking it can't reach Home Assistant!
|
||||||
|
* Also a Raspberry Pi Pico W *should* work too, but I haven't tested it yet
|
||||||
|
* Waveshare Bosch BME280 sensor: https://www.amazon.com/dp/B07Q47DFXS
|
||||||
|
* Elegoo SSD1306 LCD: https://www.amazon.com/dp/B0FSRQG23K
|
||||||
|
|
||||||
Connect the BME280 sensor to your Pico and power on.
|
Most (or all?) of these components can be swapped for equivalent components. You just need to make sure the code is compatible with whatever board you choose (not likely), is a genuine BME280 sensor, and an SSD1306 display.
|
||||||
|
|
||||||
Head on to your Home Assistant instance and enjoy!
|
Support for other hardware types may come in the future, but if there's a specific sensor or board you're interested in, please submit an issue or pull request!
|
||||||
|
|
||||||
|
### Prototyping
|
||||||
|
|
||||||
|
To help with debugging/testing deployments, I also used these components:
|
||||||
|
|
||||||
|
* Elegoo 400-pin Breadboard: https://www.amazon.com/dp/B01EV640I6
|
||||||
|
* Freenove Raspberry Pi Pico Breakout Board: https://www.amazon.com/dp/B0BFB53Y2N
|
||||||
|
* Elegoo 20cm Jumper Cables 120 count: https://www.amazon.com/dp/B01EV70C78
|
||||||
|
|
||||||
|
These are not necessary to run the app, but can be useful for testing and/or flashing several nodes.
|
||||||
|
|
||||||
|
## Software Deployment
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* [MicroPython](https://micropython.org/download/) - MicroPython downloads
|
||||||
|
* [mpremote](https://docs.micropython.org/en/latest/reference/mpremote.html) - MicroPython remote serial tool
|
||||||
|
* (Linux users) Your user should be a part of the `dialout` group
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
1. Connect your BME280 sensor and SSD1306 display to your Pico.
|
||||||
|
1. You *may* need a breadboard for this, else you can use splitters.
|
||||||
|
2. Connect your Pico to your PC via a micro-USB to USB type-A cable.
|
||||||
|
3. [Flash your Raspberry Pi Pico with MicroPython](https://docs.micropython.org/en/latest/rp2/tutorial/reset.html).
|
||||||
|
1. This has been tested with MicroPython 1.29, but other versions should work.
|
||||||
|
4. Clone this git repository: `git clone https://git.metaunix.net/BitGoblin/pico-time`.
|
||||||
|
5. `cd` into the repo: `cd pico-time`.
|
||||||
|
6. Install Python dependencies via mpremote: `mpremote mip install ./package.json`.
|
||||||
|
7. Copy `config.example.py` to `config.py` and edit the settings to match your environment.
|
||||||
|
8. Copy the Python code over to the Pico:
|
||||||
|
1. `mpremote cp main.py :main.py`
|
||||||
|
2. `mpremote cp config.py :config.py`
|
||||||
|
9. Reset the Pico: `mpremote reset`
|
||||||
|
|
||||||
|
There's also `./install.sh` if you'd like to run steps 6-9 more quickly.
|
||||||
|
|
||||||
|
If it worked you should see the display show a message about connecting to WiFi, then eventually a readout of the current temperature to the SSD1306 display. If it failed, you can monitor the app via `mpremote repl` or using an IDE like [Thonny](https://thonny.org/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ MQTT_PASS = "mqtt_password" # MQTT password (leave blank for no login)
|
|||||||
NODE_NAME = "Room Weather" # Name that's displayed on HA Device Card
|
NODE_NAME = "Room Weather" # Name that's displayed on HA Device Card
|
||||||
NODE_ID = "room_env" # Unique slug for entity_id and MQTT topic path
|
NODE_ID = "room_env" # Unique slug for entity_id and MQTT topic path
|
||||||
CLIENT_ID = f"pico2w_{NODE_ID}" # Unique client ID used by the MQTT broker
|
CLIENT_ID = f"pico2w_{NODE_ID}" # Unique client ID used by the MQTT broker
|
||||||
|
|
||||||
|
# Sensor selection: "bme280" or "sht41"
|
||||||
|
SENSOR_TYPE = "bme280"
|
||||||
|
|||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
APP_FILES=('main.py' 'config.py' 'lib/')
|
||||||
|
APP_PACKAGES='./package.json'
|
||||||
|
|
||||||
|
# Copy required files
|
||||||
|
mpremote cp -r "${APP_FILES[@]}" :
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
mpremote mip install ./package.json
|
||||||
|
|
||||||
|
# Reset the pico
|
||||||
|
mpremote reset
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
import struct
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class BME280:
|
||||||
|
DEFAULT_ADDR = 0x77
|
||||||
|
|
||||||
|
def __init__(self, i2c, addr=DEFAULT_ADDR):
|
||||||
|
self.i2c = i2c
|
||||||
|
self.addr = addr
|
||||||
|
|
||||||
|
# Reset sensor
|
||||||
|
self.i2c.writeto_mem(self.addr, 0xE0, b"\xb6")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
self._read_calibration_data()
|
||||||
|
|
||||||
|
# Humidity oversampling x1
|
||||||
|
self.i2c.writeto_mem(self.addr, 0xF2, b"\x01")
|
||||||
|
# Pressure oversampling x1, Temp oversampling x1, Normal mode
|
||||||
|
self.i2c.writeto_mem(self.addr, 0xF4, b"\x27")
|
||||||
|
# Standby 1000ms, filter off
|
||||||
|
self.i2c.writeto_mem(self.addr, 0xF5, b"\xa0")
|
||||||
|
|
||||||
|
def _read_calibration_data(self):
|
||||||
|
calib = self.i2c.readfrom_mem(self.addr, 0x88, 24)
|
||||||
|
c = struct.unpack("<HhhHhhhhhhhh", calib)
|
||||||
|
self.dig_t1, self.dig_t2, self.dig_t3 = c[0], c[1], c[2]
|
||||||
|
self.dig_p1, self.dig_p2, self.dig_p3 = c[3], c[4], c[5]
|
||||||
|
self.dig_p4, self.dig_p5, self.dig_p6 = c[6], c[7], c[8]
|
||||||
|
self.dig_p7, self.dig_p8, self.dig_p9 = c[9], c[10], c[11]
|
||||||
|
|
||||||
|
self.dig_h1 = self.i2c.readfrom_mem(self.addr, 0xA1, 1)[0]
|
||||||
|
h = struct.unpack("<hB", self.i2c.readfrom_mem(self.addr, 0xE1, 3)[:3])
|
||||||
|
self.dig_h2, self.dig_h3 = h[0], h[1]
|
||||||
|
|
||||||
|
e4 = self.i2c.readfrom_mem(self.addr, 0xE4, 2)
|
||||||
|
self.dig_h4 = (e4[0] << 4) | (e4[1] & 0x0F)
|
||||||
|
|
||||||
|
e5_e6 = self.i2c.readfrom_mem(self.addr, 0xE5, 2)
|
||||||
|
self.dig_h5 = (e5_e6[0] >> 4) | (e5_e6[1] << 4)
|
||||||
|
|
||||||
|
self.dig_h6 = struct.unpack("<b", self.i2c.readfrom_mem(self.addr, 0xE7, 1))[0]
|
||||||
|
|
||||||
|
def read_values(self):
|
||||||
|
raw = self.i2c.readfrom_mem(self.addr, 0xF7, 8)
|
||||||
|
raw_p = ((raw[0] << 16) | (raw[1] << 8) | raw[2]) >> 4
|
||||||
|
raw_t = ((raw[3] << 16) | (raw[4] << 8) | raw[5]) >> 4
|
||||||
|
raw_h = (raw[6] << 8) | raw[7]
|
||||||
|
|
||||||
|
# Temperature calculation (Celsius)
|
||||||
|
var1 = (((raw_t >> 3) - (self.dig_t1 << 1)) * self.dig_t2) >> 11
|
||||||
|
var2 = (
|
||||||
|
(((raw_t >> 4) - self.dig_t1) * ((raw_t >> 4) - self.dig_t1)) >> 12
|
||||||
|
) * self.dig_t3 >> 14
|
||||||
|
t_fine = var1 + var2
|
||||||
|
temp = ((t_fine * 5 + 128) >> 8) / 100.0
|
||||||
|
|
||||||
|
# Pressure calculation (hPa)
|
||||||
|
p_var1 = t_fine - 128000
|
||||||
|
p_var2 = (
|
||||||
|
p_var1 * p_var1 * self.dig_p6 + ((p_var1 * self.dig_p5) << 17) + (self.dig_p4 << 35)
|
||||||
|
)
|
||||||
|
p_var1 = ((p_var1 * p_var1 * self.dig_p3) >> 8) + ((p_var1 * self.dig_p2) << 12)
|
||||||
|
p_var1 = (((1 << 47) + p_var1) * self.dig_p1) >> 33
|
||||||
|
|
||||||
|
if p_var1 == 0:
|
||||||
|
pressure = 0.0
|
||||||
|
else:
|
||||||
|
p = 1048576 - raw_p
|
||||||
|
p = (((p << 31) - p_var2) * 3125) // p_var1
|
||||||
|
p_var1 = (self.dig_p9 * (p >> 13) * (p >> 13)) >> 25
|
||||||
|
p_var2 = (self.dig_p8 * p) >> 19
|
||||||
|
p = ((p + p_var1 + p_var2) >> 8) + (self.dig_p7 << 4)
|
||||||
|
pressure = (p / 256.0) / 100.0
|
||||||
|
|
||||||
|
# Humidity calculation (%)
|
||||||
|
h_var = t_fine - 76800
|
||||||
|
h_var = ((((raw_h << 14) - (self.dig_h4 << 20) - (self.dig_h5 * h_var)) + 16384) >> 15) * (
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(
|
||||||
|
(((h_var * self.dig_h6) >> 10) * (((h_var * self.dig_h3) >> 11) + 32768))
|
||||||
|
>> 10
|
||||||
|
)
|
||||||
|
+ 2097152
|
||||||
|
)
|
||||||
|
* self.dig_h2
|
||||||
|
+ 8192
|
||||||
|
)
|
||||||
|
>> 14
|
||||||
|
)
|
||||||
|
h_var = h_var - (((((h_var >> 15) * (h_var >> 15)) >> 7) * self.dig_h1) >> 4)
|
||||||
|
h_var = max(0, min(h_var, 419430400))
|
||||||
|
humidity = (h_var >> 12) / 1024.0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"temperature": round(temp, 2),
|
||||||
|
"pressure": round(pressure, 2),
|
||||||
|
"humidity": round(humidity, 2),
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class SHT41:
|
||||||
|
DEFAULT_ADDR = 0x44
|
||||||
|
CMD_MEASURE_HIGH_PRECISION = 0xFD
|
||||||
|
|
||||||
|
def __init__(self, i2c, addr=DEFAULT_ADDR):
|
||||||
|
self.i2c = i2c
|
||||||
|
self.addr = addr
|
||||||
|
# Soft reset
|
||||||
|
self.i2c.writeto(self.addr, b"\x94")
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
def read_values(self):
|
||||||
|
# Trigger high precision measurement
|
||||||
|
self.i2c.writeto(self.addr, bytes([self.CMD_MEASURE_HIGH_PRECISION]))
|
||||||
|
# Conversion takes up to 8.2ms
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
# 6 bytes: [Temp_MSB, Temp_LSB, CRC, Hum_MSB, Hum_LSB, CRC]
|
||||||
|
data = self.i2c.readfrom(self.addr, 6)
|
||||||
|
|
||||||
|
raw_t = (data[0] << 8) | data[1]
|
||||||
|
raw_h = (data[3] << 8) | data[4]
|
||||||
|
|
||||||
|
# Sensirion SHT4x conversion equations
|
||||||
|
temp = -45.0 + 175.0 * (raw_t / 65535.0)
|
||||||
|
hum = -6.0 + 125.0 * (raw_h / 65535.0)
|
||||||
|
hum = max(0.0, min(hum, 100.0))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"temperature": round(temp, 2),
|
||||||
|
"humidity": round(hum, 2),
|
||||||
|
"pressure": None,
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleMQTT:
|
||||||
|
def __init__(self, host, port=1883):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def connect(self, client_id, username=None, password=None):
|
||||||
|
addr_info = socket.getaddrinfo(self.host, self.port)
|
||||||
|
addr = addr_info[0][-1]
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.sock.connect(addr)
|
||||||
|
|
||||||
|
# Flags: Clean session (bit 1) + optional credentials
|
||||||
|
flags = 0x02
|
||||||
|
if username:
|
||||||
|
flags |= 0x80
|
||||||
|
if password:
|
||||||
|
flags |= 0x40
|
||||||
|
|
||||||
|
# Variable header: Protocol Name (MQTT) + Level (4) + Flags + KeepAlive (60s)
|
||||||
|
var_header = bytearray(
|
||||||
|
[0x00, 0x04, ord("M"), ord("Q"), ord("T"), ord("T"), 0x04, flags, 0x00, 0x3C]
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = bytearray()
|
||||||
|
payload.extend(self._encode_str(client_id))
|
||||||
|
if username:
|
||||||
|
payload.extend(self._encode_str(username))
|
||||||
|
if password:
|
||||||
|
payload.extend(self._encode_str(password))
|
||||||
|
|
||||||
|
body = var_header + payload
|
||||||
|
packet = bytearray([0x10]) + self._encode_len(len(body)) + body
|
||||||
|
self.sock.write(packet)
|
||||||
|
|
||||||
|
# Read CONNACK (4 bytes: 0x20, 0x02, ack_flags, return_code)
|
||||||
|
resp = self.sock.read(4)
|
||||||
|
if not resp or resp[0] != 0x20 or resp[3] != 0x00:
|
||||||
|
code = resp[3] if resp and len(resp) >= 4 else "nil"
|
||||||
|
raise RuntimeError(f"MQTT connection failed: code {code}")
|
||||||
|
|
||||||
|
def publish(self, topic, payload, retain=False):
|
||||||
|
cmd = 0x30 | (0x01 if retain else 0x00)
|
||||||
|
if isinstance(payload, str):
|
||||||
|
payload = payload.encode("utf-8")
|
||||||
|
body = self._encode_str(topic) + payload
|
||||||
|
packet = bytearray([cmd]) + self._encode_len(len(body)) + body
|
||||||
|
self.sock.write(packet)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
try:
|
||||||
|
if self.sock:
|
||||||
|
self.sock.write(bytearray([0xE0, 0x00]))
|
||||||
|
self.sock.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def _encode_str(self, s):
|
||||||
|
raw = s.encode("utf-8")
|
||||||
|
return struct.pack("!H", len(raw)) + raw
|
||||||
|
|
||||||
|
def _encode_len(self, length):
|
||||||
|
encoded = bytearray()
|
||||||
|
while True:
|
||||||
|
digit = length % 128
|
||||||
|
length //= 128
|
||||||
|
if length > 0:
|
||||||
|
digit |= 0x80
|
||||||
|
encoded.append(digit)
|
||||||
|
if length == 0:
|
||||||
|
break
|
||||||
|
return encoded
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import socket
|
|
||||||
import struct
|
|
||||||
import time
|
import time
|
||||||
from machine import Pin, SoftI2C
|
from machine import Pin, SoftI2C
|
||||||
|
import json
|
||||||
import network
|
import network
|
||||||
|
from simple_mqtt import SimpleMQTT
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Configuration Loader
|
# Configuration Loader
|
||||||
@@ -15,206 +15,29 @@ except ImportError:
|
|||||||
while True:
|
while True:
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# Default NODE_NAME to NODE_ID if not explicitly provided in config.py
|
|
||||||
NODE_NAME = getattr(Config, "NODE_NAME", Config.NODE_ID)
|
NODE_NAME = getattr(Config, "NODE_NAME", Config.NODE_ID)
|
||||||
|
SENSOR_TYPE = getattr(Config, "SENSOR_TYPE", "bme280").lower()
|
||||||
STATE_TOPIC = f"homeassistant/sensor/{Config.NODE_ID}/state"
|
STATE_TOPIC = f"homeassistant/sensor/{Config.NODE_ID}/state"
|
||||||
|
|
||||||
|
# Dynamic sensor driver import
|
||||||
# -------------------------------------------------------------
|
if SENSOR_TYPE == "sht41":
|
||||||
# Minimal MQTT 3.1.1 Client
|
from sht41 import SHT41
|
||||||
# -------------------------------------------------------------
|
elif SENSOR_TYPE == "bme280":
|
||||||
class SimpleMQTT:
|
from bme280 import BME280
|
||||||
def __init__(self, host, port=1883):
|
|
||||||
self.host = host
|
|
||||||
self.port = port
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def connect(self, client_id, username=None, password=None):
|
|
||||||
addr_info = socket.getaddrinfo(self.host, self.port)
|
|
||||||
addr = addr_info[0][-1]
|
|
||||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
self.sock.connect(addr)
|
|
||||||
|
|
||||||
# Flags: Clean session (bit 1) + optional credentials
|
|
||||||
flags = 0x02
|
|
||||||
if username:
|
|
||||||
flags |= 0x80
|
|
||||||
if password:
|
|
||||||
flags |= 0x40
|
|
||||||
|
|
||||||
# Variable header: Protocol Name (MQTT) + Level (4) + Flags + KeepAlive (60s)
|
|
||||||
var_header = bytearray(
|
|
||||||
[0x00, 0x04, ord("M"), ord("Q"), ord("T"), ord("T"), 0x04, flags, 0x00, 0x3C]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Payload
|
|
||||||
payload = bytearray()
|
|
||||||
payload.extend(self._encode_str(client_id))
|
|
||||||
if username:
|
|
||||||
payload.extend(self._encode_str(username))
|
|
||||||
if password:
|
|
||||||
payload.extend(self._encode_str(password))
|
|
||||||
|
|
||||||
body = var_header + payload
|
|
||||||
packet = bytearray([0x10]) + self._encode_len(len(body)) + body
|
|
||||||
self.sock.write(packet)
|
|
||||||
|
|
||||||
# Read CONNACK (4 bytes: 0x20, 0x02, ack_flags, return_code)
|
|
||||||
resp = self.sock.read(4)
|
|
||||||
if not resp or resp[0] != 0x20 or resp[3] != 0x00:
|
|
||||||
code = resp[3] if resp and len(resp) >= 4 else "nil"
|
|
||||||
raise RuntimeError(f"MQTT connection failed: code {code}")
|
|
||||||
|
|
||||||
def publish(self, topic, payload, retain=False):
|
|
||||||
cmd = 0x30 | (0x01 if retain else 0x00)
|
|
||||||
if isinstance(payload, str):
|
|
||||||
payload = payload.encode("utf-8")
|
|
||||||
body = self._encode_str(topic) + payload
|
|
||||||
packet = bytearray([cmd]) + self._encode_len(len(body)) + body
|
|
||||||
self.sock.write(packet)
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
try:
|
|
||||||
if self.sock:
|
|
||||||
self.sock.write(bytearray([0xE0, 0x00]))
|
|
||||||
self.sock.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
def _encode_str(self, s):
|
|
||||||
raw = s.encode("utf-8")
|
|
||||||
return struct.pack("!H", len(raw)) + raw
|
|
||||||
|
|
||||||
def _encode_len(self, length):
|
|
||||||
encoded = bytearray()
|
|
||||||
while True:
|
|
||||||
digit = length % 128
|
|
||||||
length //= 128
|
|
||||||
if length > 0:
|
|
||||||
digit |= 0x80
|
|
||||||
encoded.append(digit)
|
|
||||||
if length == 0:
|
|
||||||
break
|
|
||||||
return encoded
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
# BME280 Driver (I2C)
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
class BME280:
|
|
||||||
DEFAULT_ADDR = 0x77
|
|
||||||
|
|
||||||
def __init__(self, i2c, addr=DEFAULT_ADDR):
|
|
||||||
self.i2c = i2c
|
|
||||||
self.addr = addr
|
|
||||||
|
|
||||||
# Reset sensor
|
|
||||||
self.i2c.writeto_mem(self.addr, 0xE0, b"\xb6")
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
self._read_calibration_data()
|
|
||||||
|
|
||||||
# Humidity oversampling x1
|
|
||||||
self.i2c.writeto_mem(self.addr, 0xF2, b"\x01")
|
|
||||||
# Pressure oversampling x1, Temp oversampling x1, Normal mode
|
|
||||||
self.i2c.writeto_mem(self.addr, 0xF4, b"\x27")
|
|
||||||
# Standby 1000ms, filter off
|
|
||||||
self.i2c.writeto_mem(self.addr, 0xF5, b"\xa0")
|
|
||||||
|
|
||||||
def _read_calibration_data(self):
|
|
||||||
calib = self.i2c.readfrom_mem(self.addr, 0x88, 24)
|
|
||||||
c = struct.unpack("<HhhHhhhhhhhh", calib)
|
|
||||||
self.dig_t1, self.dig_t2, self.dig_t3 = c[0], c[1], c[2]
|
|
||||||
self.dig_p1, self.dig_p2, self.dig_p3 = c[3], c[4], c[5]
|
|
||||||
self.dig_p4, self.dig_p5, self.dig_p6 = c[6], c[7], c[8]
|
|
||||||
self.dig_p7, self.dig_p8, self.dig_p9 = c[9], c[10], c[11]
|
|
||||||
|
|
||||||
self.dig_h1 = self.i2c.readfrom_mem(self.addr, 0xA1, 1)[0]
|
|
||||||
h = struct.unpack("<hB", self.i2c.readfrom_mem(self.addr, 0xE1, 3)[:3])
|
|
||||||
self.dig_h2, self.dig_h3 = h[0], h[1]
|
|
||||||
|
|
||||||
e4 = self.i2c.readfrom_mem(self.addr, 0xE4, 2)
|
|
||||||
self.dig_h4 = (e4[0] << 4) | (e4[1] & 0x0F)
|
|
||||||
|
|
||||||
e5_e6 = self.i2c.readfrom_mem(self.addr, 0xE5, 2)
|
|
||||||
self.dig_h5 = (e5_e6[0] >> 4) | (e5_e6[1] << 4)
|
|
||||||
|
|
||||||
self.dig_h6 = struct.unpack("<b", self.i2c.readfrom_mem(self.addr, 0xE7, 1))[0]
|
|
||||||
|
|
||||||
def read_values(self):
|
|
||||||
raw = self.i2c.readfrom_mem(self.addr, 0xF7, 8)
|
|
||||||
raw_p = ((raw[0] << 16) | (raw[1] << 8) | raw[2]) >> 4
|
|
||||||
raw_t = ((raw[3] << 16) | (raw[4] << 8) | raw[5]) >> 4
|
|
||||||
raw_h = (raw[6] << 8) | raw[7]
|
|
||||||
|
|
||||||
# Temperature calculation (Celsius)
|
|
||||||
var1 = (((raw_t >> 3) - (self.dig_t1 << 1)) * self.dig_t2) >> 11
|
|
||||||
var2 = (
|
|
||||||
(((raw_t >> 4) - self.dig_t1) * ((raw_t >> 4) - self.dig_t1)) >> 12
|
|
||||||
) * self.dig_t3 >> 14
|
|
||||||
t_fine = var1 + var2
|
|
||||||
temp = ((t_fine * 5 + 128) >> 8) / 100.0
|
|
||||||
|
|
||||||
# Pressure calculation (hPa)
|
|
||||||
p_var1 = t_fine - 128000
|
|
||||||
p_var2 = (
|
|
||||||
p_var1 * p_var1 * self.dig_p6 + ((p_var1 * self.dig_p5) << 17) + (self.dig_p4 << 35)
|
|
||||||
)
|
|
||||||
p_var1 = ((p_var1 * p_var1 * self.dig_p3) >> 8) + ((p_var1 * self.dig_p2) << 12)
|
|
||||||
p_var1 = (((1 << 47) + p_var1) * self.dig_p1) >> 33
|
|
||||||
|
|
||||||
if p_var1 == 0:
|
|
||||||
pressure = 0.0
|
|
||||||
else:
|
else:
|
||||||
p = 1048576 - raw_p
|
raise ValueError(f"Unsupported SENSOR_TYPE '{SENSOR_TYPE}'. Use 'bme280' or 'sht41'.")
|
||||||
p = (((p << 31) - p_var2) * 3125) // p_var1
|
|
||||||
p_var1 = (self.dig_p9 * (p >> 13) * (p >> 13)) >> 25
|
|
||||||
p_var2 = (self.dig_p8 * p) >> 19
|
|
||||||
p = ((p + p_var1 + p_var2) >> 8) + (self.dig_p7 << 4)
|
|
||||||
pressure = (p / 256.0) / 100.0
|
|
||||||
|
|
||||||
# Humidity calculation (%)
|
|
||||||
h_var = t_fine - 76800
|
|
||||||
h_var = ((((raw_h << 14) - (self.dig_h4 << 20) - (self.dig_h5 * h_var)) + 16384) >> 15) * (
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(((h_var * self.dig_h6) >> 10) * (((h_var * self.dig_h3) >> 11) + 32768))
|
|
||||||
>> 10
|
|
||||||
)
|
|
||||||
+ 2097152
|
|
||||||
)
|
|
||||||
* self.dig_h2
|
|
||||||
+ 8192
|
|
||||||
)
|
|
||||||
>> 14
|
|
||||||
)
|
|
||||||
h_var = h_var - (((((h_var >> 15) * (h_var >> 15)) >> 7) * self.dig_h1) >> 4)
|
|
||||||
h_var = max(0, min(h_var, 419430400))
|
|
||||||
humidity = (h_var >> 12) / 1024.0
|
|
||||||
|
|
||||||
return {
|
|
||||||
"temperature": round(temp, 2),
|
|
||||||
"pressure": round(pressure, 2),
|
|
||||||
"humidity": round(humidity, 2),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Home Assistant MQTT Discovery
|
# Home Assistant MQTT Discovery
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
def register_ha_discovery(mqtt):
|
def register_ha_discovery(mqtt, sensor_type):
|
||||||
# Device naming uses NODE_NAME so each board has its own clean device card in HA
|
device_info = {
|
||||||
device_info = (
|
"identifiers": [Config.NODE_ID],
|
||||||
f'"device":{{'
|
"name": NODE_NAME,
|
||||||
f'"identifiers":["{Config.NODE_ID}"],'
|
"model": f"Pico 2 W ({sensor_type.upper()})",
|
||||||
f'"name":"{NODE_NAME}",'
|
"manufacturer": "Raspberry Pi",
|
||||||
f'"model":"Pico 2 W",'
|
}
|
||||||
f'"manufacturer":"Raspberry Pi"'
|
|
||||||
f"}}"
|
|
||||||
)
|
|
||||||
|
|
||||||
configs = [
|
configs = [
|
||||||
{
|
{
|
||||||
@@ -231,35 +54,39 @@ def register_ha_discovery(mqtt):
|
|||||||
"cls": "humidity",
|
"cls": "humidity",
|
||||||
"val_tpl": "{{ value_json.humidity }}",
|
"val_tpl": "{{ value_json.humidity }}",
|
||||||
},
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if sensor_type == "bme280":
|
||||||
|
configs.append(
|
||||||
{
|
{
|
||||||
"id": "pressure",
|
"id": "pressure",
|
||||||
"name": "Pressure",
|
"name": "Pressure",
|
||||||
"unit": "hPa",
|
"unit": "hPa",
|
||||||
"cls": "atmospheric_pressure",
|
"cls": "atmospheric_pressure",
|
||||||
"val_tpl": "{{ value_json.pressure }}",
|
"val_tpl": "{{ value_json.pressure }}",
|
||||||
},
|
}
|
||||||
]
|
)
|
||||||
|
|
||||||
for c in configs:
|
for c in configs:
|
||||||
topic = f"homeassistant/sensor/{Config.NODE_ID}/{c['id']}/config"
|
topic = f"homeassistant/sensor/{Config.NODE_ID}/{c['id']}/config"
|
||||||
unique_id = f"{Config.NODE_ID}_{c['id']}"
|
payload = {
|
||||||
payload = (
|
"name": c["name"],
|
||||||
f'{{"name":"{c["name"]}",'
|
"has_entity_name": True,
|
||||||
f'"has_entity_name":true,'
|
"unique_id": f"{Config.NODE_ID}_{c['id']}",
|
||||||
f'"unique_id":"{unique_id}",'
|
"device_class": c["cls"],
|
||||||
f'"device_class":"{c["cls"]}",'
|
"state_class": "measurement",
|
||||||
f'"state_class":"measurement",'
|
"unit_of_measurement": c["unit"],
|
||||||
f'"unit_of_measurement":"{c["unit"]}",'
|
"state_topic": STATE_TOPIC,
|
||||||
f'"state_topic":"{STATE_TOPIC}",'
|
"value_template": c["val_tpl"],
|
||||||
f'"value_template":"{c["val_tpl"]}",'
|
"device": device_info,
|
||||||
f"{device_info}}}"
|
}
|
||||||
)
|
|
||||||
mqtt.publish(topic, payload, retain=True)
|
mqtt.publish(topic, json.dumps(payload), retain=True)
|
||||||
print(f"Registered HA discovery: {c['id']}")
|
print(f"Registered HA discovery: {c['id']}")
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Main Execution Loop
|
# Wi-Fi Connection
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
def connect_wifi():
|
def connect_wifi():
|
||||||
wlan = network.WLAN(network.STA_IF)
|
wlan = network.WLAN(network.STA_IF)
|
||||||
@@ -277,10 +104,30 @@ def connect_wifi():
|
|||||||
print(f"Wi-Fi connected. IP: {wlan.ifconfig()[0]}")
|
print(f"Wi-Fi connected. IP: {wlan.ifconfig()[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Panic Handler
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
def panic(err=None):
|
||||||
|
if err:
|
||||||
|
print(f"Fatal error: {err}")
|
||||||
|
led = Pin("LED", Pin.OUT)
|
||||||
|
while True:
|
||||||
|
led.toggle()
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Main Execution Loop
|
||||||
|
# -------------------------------------------------------------
|
||||||
def main():
|
def main():
|
||||||
# Adjust SDA/SCL pins according to your wiring
|
i2c = SoftI2C(sda=Pin(4), scl=Pin(5), freq=100000)
|
||||||
i2c = SoftI2C(sda=Pin(16), scl=Pin(17), freq=100000)
|
|
||||||
bme = BME280(i2c)
|
if SENSOR_TYPE == "sht41":
|
||||||
|
print("Using SHT41 sensor (0x44)")
|
||||||
|
sensor = SHT41(i2c)
|
||||||
|
else:
|
||||||
|
print("Using BME280 sensor (0x77)")
|
||||||
|
sensor = BME280(i2c)
|
||||||
|
|
||||||
connect_wifi()
|
connect_wifi()
|
||||||
|
|
||||||
@@ -293,19 +140,16 @@ def main():
|
|||||||
mqtt.connect(Config.CLIENT_ID, mqtt_user, mqtt_pass)
|
mqtt.connect(Config.CLIENT_ID, mqtt_user, mqtt_pass)
|
||||||
print("MQTT connected.")
|
print("MQTT connected.")
|
||||||
|
|
||||||
# Publish discovery retained messages
|
register_ha_discovery(mqtt, SENSOR_TYPE)
|
||||||
register_ha_discovery(mqtt)
|
|
||||||
|
|
||||||
# Telemetry loop
|
|
||||||
interval = getattr(Config, "UPDATE_INTERVAL", 30)
|
interval = getattr(Config, "UPDATE_INTERVAL", 30)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = bme.read_values()
|
data = sensor.read_values()
|
||||||
state_json = (
|
|
||||||
f'{{"temperature":{data["temperature"]},'
|
payload = {k: v for k, v in data.items() if v is not None}
|
||||||
f'"humidity":{data["humidity"]},'
|
state_json = json.dumps(payload)
|
||||||
f'"pressure":{data["pressure"]}}}'
|
|
||||||
)
|
|
||||||
mqtt.publish(STATE_TOPIC, state_json)
|
mqtt.publish(STATE_TOPIC, state_json)
|
||||||
print(f"[{NODE_NAME}] Published: {state_json}")
|
print(f"[{NODE_NAME}] Published: {state_json}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -322,4 +166,10 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
time.sleep(2)
|
||||||
|
Pin("LED", Pin.OUT).on()
|
||||||
|
|
||||||
|
try:
|
||||||
main()
|
main()
|
||||||
|
except Exception as e:
|
||||||
|
panic(e)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"deps": [
|
||||||
|
["ssd1306", "0.1.0"]
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "pico-thermo"
|
name = "pico-thermo"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "MicroPython MQTT app to read BME280 sensor into Home Assistant"
|
description = "MicroPython MQTT app to read BME280 sensor into Home Assistant"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
@@ -9,7 +9,7 @@ authors = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py311"
|
target-version = "py312"
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
|
|||||||
Reference in New Issue
Block a user