12 Commits

Author SHA1 Message Date
gballan dc413b0336 Version bump to v0.2.0
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-12 17:35:38 -04:00
gballan cc022e4bf8 Fixed linting issues
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-12 17:35:04 -04:00
gballan d607a1ed29 Reworked some JSON building using the json.dumps() function
ci/woodpecker/push/woodpecker Pipeline failed
2026-09-12 17:32:34 -04:00
gballan 2768616be4 Refactored some classes into separate files in lib/
ci/woodpecker/push/woodpecker Pipeline failed
2026-09-12 17:25:55 -04:00
gballan 31c7f870e0 [Issue #4] - Adding support for sht4x sensors, configurable via the SENSOR_TYPE parameter
ci/woodpecker/push/woodpecker Pipeline failed
2026-09-12 17:19:56 -04:00
gballan 41654932e5 Adding shell script to more easily install the app
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-12 16:57:33 -04:00
gballan b33d3d4cdb [Issue #5] - Adding setup instruction step to install all dependencies via requirements.txt
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-08 21:46:04 -04:00
gballan 2d2e31ec29 Updated README
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-07 22:55:31 -04:00
gballan 8b5852c012 Version bump to v0.1.1
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2026-09-06 01:27:32 -04:00
gballan 7dbabc902b Adding CI badge
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-06 01:26:42 -04:00
gballan 2b4faca80d [Issue #3] - Fixing small linter issue
ci/woodpecker/push/woodpecker Pipeline was successful
2026-09-06 01:25:14 -04:00
gballan d28ff065f5 Adding short delay and LED indicator to show that the app is running
ci/woodpecker/push/woodpecker Pipeline failed
2026-09-06 01:21:59 -04:00
10 changed files with 366 additions and 238 deletions
-1
View File
@@ -18,7 +18,6 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/
lib64/ lib64/
parts/ parts/
sdist/ sdist/
+50 -7
View File
@@ -1,18 +1,61 @@
# pico-thermo # pico-thermo
MicroPython app to read data from BME280 sensor and send it to Home Assistant ![Woodpecker CI badge](https://builds.metaunix.net/api/badges/90/status.svg)
![shields.io version badge](https://img.shields.io/gitea/v/release/BitGoblin/pico-thermo?gitea_url=https://git.metaunix.net)
## 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
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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),
}
+36
View File
@@ -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,
}
+78
View File
@@ -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
+72 -222
View File
@@ -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): else:
self.host = host raise ValueError(f"Unsupported SENSOR_TYPE '{SENSOR_TYPE}'. Use 'bme280' or 'sht41'.")
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:
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),
}
# ------------------------------------------------------------- # -------------------------------------------------------------
# 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)
+5
View File
@@ -0,0 +1,5 @@
{
"deps": [
["ssd1306", "0.1.0"]
]
}
+2 -2
View File
@@ -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]