Skip to content

Commit b0203ad

Browse files
committed
fix(env): fall through settings.toml, dotenv, os.getenv
1 parent dc0555a commit b0203ad

1 file changed

Lines changed: 129 additions & 35 deletions

File tree

fake_bme280/basic.py

Lines changed: 129 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,25 @@
2626
https://circuitpython.org/downloads
2727
"""
2828
import math
29+
import os
30+
import random
2931
import socket as pool
3032
import ssl
3133
import typing # pylint: disable=unused-import
32-
import toml
3334
from micropython import const
3435
import adafruit_requests
3536
from fake_bme280.protocol import I2C_Impl, SPI_Impl
3637

38+
try:
39+
import toml
40+
except ImportError:
41+
toml = None
42+
43+
try:
44+
import dotenv
45+
except ImportError:
46+
dotenv = None
47+
3748
try:
3849
from busio import I2C, SPI
3950
from digitalio import DigitalInOut
@@ -75,21 +86,62 @@
7586
_BME280_REGISTER_TEMPDATA = const(0xFA)
7687
_BME280_REGISTER_HUMIDDATA = const(0xFD)
7788

78-
# Load the settings.toml file
79-
toml_config = toml.load("settings.toml")
80-
81-
# OpenWeatherMap API
82-
# GET weather data for a specific location
83-
DATA_SOURCE = (
84-
"http://api.openweathermap.org/data/2.5/weather?q="
85-
+ toml_config["openweather_location"]
86-
+ "&units="
87-
+ toml_config["openweather_units"]
88-
+ "&mode=json"
89-
+ "&appid="
90-
+ toml_config["openweather_token"]
89+
_OPENWEATHER_KEYS = (
90+
"openweather_location",
91+
"openweather_units",
92+
"openweather_token",
9193
)
9294

95+
_SETTINGS_TOML_PATH = "settings.toml"
96+
97+
98+
def _load_openweather_settings() -> dict:
99+
"""Return OpenWeatherMap settings, trying settings.toml, then .env
100+
(python-dotenv), then os.getenv, per key in that order. Raises
101+
``RuntimeError`` if settings.toml is malformed or any key is missing."""
102+
toml_values = {}
103+
if toml is not None:
104+
try:
105+
toml_values = toml.load(_SETTINGS_TOML_PATH)
106+
except FileNotFoundError:
107+
pass
108+
except toml.TomlDecodeError as err:
109+
raise RuntimeError(
110+
"Could not parse %s: %s" % (_SETTINGS_TOML_PATH, err)
111+
) from err
112+
113+
dotenv_values = dotenv.dotenv_values() if dotenv is not None else {}
114+
115+
settings = {}
116+
for key in _OPENWEATHER_KEYS:
117+
value = toml_values.get(key) or dotenv_values.get(key) or os.getenv(key)
118+
if value:
119+
settings[key] = value
120+
121+
missing = [k for k in _OPENWEATHER_KEYS if k not in settings]
122+
if missing:
123+
raise RuntimeError(
124+
"fake_bme280 is missing required OpenWeatherMap setting(s): "
125+
+ ", ".join(missing)
126+
+ ". Provide them via settings.toml, a .env file (python-dotenv), "
127+
"or environment variables. Or construct the sensor with "
128+
"use_openweather=False for random fake readings (no network)."
129+
)
130+
return settings
131+
132+
133+
def _build_openweather_url() -> str:
134+
"""Build the OpenWeatherMap API URL from the loaded settings."""
135+
config = _load_openweather_settings()
136+
return (
137+
"http://api.openweathermap.org/data/2.5/weather?q="
138+
+ config["openweather_location"]
139+
+ "&units="
140+
+ config["openweather_units"]
141+
+ "&mode=json&appid="
142+
+ config["openweather_token"]
143+
)
144+
93145

94146
class Adafruit_BME280:
95147
"""Driver from BME280 Temperature, Humidity and Barometric Pressure sensor
@@ -101,11 +153,21 @@ class Adafruit_BME280:
101153
"""
102154

103155
# pylint: disable=too-many-instance-attributes
104-
def __init__(self, bus_implementation: typing.Union[I2C_Impl, SPI_Impl]) -> None:
105-
"""Mock a BME280 sensor object that was found on an I2C bus."""
106-
# Check device ID.
156+
def __init__(
157+
self,
158+
bus_implementation: typing.Union[I2C_Impl, SPI_Impl],
159+
use_openweather: bool = True,
160+
) -> None:
161+
"""Mock a BME280 sensor object that was found on an I2C bus.
162+
163+
:param bus_implementation: I2C or SPI implementation wrapper.
164+
:param bool use_openweather: When ``True`` (default), read settings
165+
from the environment (``settings.toml`` on CircuitPython,
166+
environment variables on CPython) and fetch live data from the
167+
OpenWeatherMap API. When ``False``, return random plausible
168+
readings with no network access and no configuration required.
169+
"""
107170
self._bus_implementation = bus_implementation
108-
# Set some reasonable defaults.
109171
self._iir_filter = IIR_FILTER_DISABLE
110172
self.overscan_humidity = OVERSCAN_X1
111173
self.overscan_temperature = OVERSCAN_X1
@@ -115,23 +177,42 @@ def __init__(self, bus_implementation: typing.Union[I2C_Impl, SPI_Impl]) -> None
115177
self.sea_level_pressure = 1013.25
116178
"""Pressure in hectoPascals at sea level. Used to calibrate `altitude`."""
117179
self._t_fine = None
118-
# Configure a CPython adafruit_requests session
119-
self.requests = adafruit_requests.Session(pool, ssl.create_default_context())
120-
self._current_forcast = None
121-
# Test call get_forecast
122-
self.get_forecast()
180+
self._use_openweather = use_openweather
181+
self._current_forecast = None
182+
self._data_source = None
183+
self.requests = None
184+
if use_openweather:
185+
self._data_source = _build_openweather_url()
186+
self.requests = adafruit_requests.Session(
187+
pool, ssl.create_default_context()
188+
)
189+
self.get_forecast()
190+
else:
191+
self._current_forecast = self._random_forecast()
192+
193+
@staticmethod
194+
def _random_forecast() -> dict:
195+
"""Generate a plausible BME280-shaped reading."""
196+
return {
197+
"main": {
198+
"temp": round(random.uniform(18.0, 28.0), 2),
199+
"pressure": round(random.uniform(1000.0, 1025.0), 2),
200+
"humidity": round(random.uniform(30.0, 70.0), 2),
201+
}
202+
}
123203

124204
def get_forecast(self):
125-
"""Fetch weather from OpenWeatherMap API"""
126-
# print("Fetching json from", DATA_SOURCE)
127-
response = self.requests.get(DATA_SOURCE)
128-
self._current_forcast = response.json()
129-
# print(self._current_forcast)
205+
"""Fetch weather from OpenWeatherMap (or generate fake data)."""
206+
if not self._use_openweather:
207+
self._current_forecast = self._random_forecast()
208+
return
209+
response = self.requests.get(self._data_source)
210+
self._current_forecast = response.json()
130211

131212
def _read_temperature(self) -> None:
132213
# Get the OpenWeather temperature and store it in _t_fine
133214
self.get_forecast()
134-
self._t_fine = self._current_forcast["main"]["temp"]
215+
self._t_fine = self._current_forecast["main"]["temp"]
135216

136217
@property
137218
def mode(self) -> int:
@@ -177,7 +258,7 @@ def pressure(self) -> float:
177258
The compensated pressure in hectoPascals.
178259
"""
179260
self._read_temperature()
180-
return self._current_forcast["main"]["pressure"]
261+
return self._current_forecast["main"]["pressure"]
181262

182263
@property
183264
def relative_humidity(self) -> float:
@@ -192,7 +273,7 @@ def humidity(self) -> float:
192273
The relative humidity in RH %
193274
"""
194275
self._read_temperature()
195-
humidity = self._current_forcast["main"]["humidity"]
276+
humidity = self._current_forecast["main"]["humidity"]
196277
if humidity > 100:
197278
return 100
198279
if humidity < 0:
@@ -253,8 +334,13 @@ class Adafruit_BME280_I2C(Adafruit_BME280):
253334
254335
"""
255336

256-
def __init__(self, i2c: I2C, address: int = 0x77) -> None: # BME280_ADDRESS
257-
super().__init__(I2C_Impl(i2c, address))
337+
def __init__(
338+
self,
339+
i2c: I2C,
340+
address: int = 0x77, # BME280_ADDRESS
341+
use_openweather: bool = True,
342+
) -> None:
343+
super().__init__(I2C_Impl(i2c, address), use_openweather=use_openweather)
258344

259345

260346
class Adafruit_BME280_SPI(Adafruit_BME280):
@@ -305,5 +391,13 @@ class Adafruit_BME280_SPI(Adafruit_BME280):
305391
306392
"""
307393

308-
def __init__(self, spi: SPI, cs: DigitalInOut, baudrate: int = 100000) -> None:
309-
super().__init__(SPI_Impl(spi, cs, baudrate))
394+
def __init__(
395+
self,
396+
spi: SPI,
397+
cs: DigitalInOut,
398+
baudrate: int = 100000,
399+
use_openweather: bool = True,
400+
) -> None:
401+
super().__init__(
402+
SPI_Impl(spi, cs, baudrate), use_openweather=use_openweather
403+
)

0 commit comments

Comments
 (0)