Skip to content

Commit 9d6f769

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

2 files changed

Lines changed: 131 additions & 35 deletions

File tree

fake_bme280/basic.py

Lines changed: 128 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,27 @@
2525
* Adafruit CircuitPython firmware for the supported boards:
2626
https://circuitpython.org/downloads
2727
"""
28+
2829
import math
30+
import os
31+
import random
2932
import socket as pool
3033
import ssl
3134
import typing # pylint: disable=unused-import
32-
import toml
3335
from micropython import const
3436
import adafruit_requests
3537
from fake_bme280.protocol import I2C_Impl, SPI_Impl
3638

39+
try:
40+
import toml
41+
except ImportError:
42+
toml = None
43+
44+
try:
45+
import dotenv
46+
except ImportError:
47+
dotenv = None
48+
3749
try:
3850
from busio import I2C, SPI
3951
from digitalio import DigitalInOut
@@ -75,21 +87,62 @@
7587
_BME280_REGISTER_TEMPDATA = const(0xFA)
7688
_BME280_REGISTER_HUMIDDATA = const(0xFD)
7789

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"]
90+
_OPENWEATHER_KEYS = (
91+
"openweather_location",
92+
"openweather_units",
93+
"openweather_token",
9194
)
9295

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

94147
class Adafruit_BME280:
95148
"""Driver from BME280 Temperature, Humidity and Barometric Pressure sensor
@@ -101,11 +154,21 @@ class Adafruit_BME280:
101154
"""
102155

103156
# 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.
157+
def __init__(
158+
self,
159+
bus_implementation: typing.Union[I2C_Impl, SPI_Impl],
160+
use_openweather: bool = True,
161+
) -> None:
162+
"""Mock a BME280 sensor object that was found on an I2C bus.
163+
164+
:param bus_implementation: I2C or SPI implementation wrapper.
165+
:param bool use_openweather: When ``True`` (default), read settings
166+
from the environment (``settings.toml`` on CircuitPython,
167+
environment variables on CPython) and fetch live data from the
168+
OpenWeatherMap API. When ``False``, return random plausible
169+
readings with no network access and no configuration required.
170+
"""
107171
self._bus_implementation = bus_implementation
108-
# Set some reasonable defaults.
109172
self._iir_filter = IIR_FILTER_DISABLE
110173
self.overscan_humidity = OVERSCAN_X1
111174
self.overscan_temperature = OVERSCAN_X1
@@ -115,23 +178,42 @@ def __init__(self, bus_implementation: typing.Union[I2C_Impl, SPI_Impl]) -> None
115178
self.sea_level_pressure = 1013.25
116179
"""Pressure in hectoPascals at sea level. Used to calibrate `altitude`."""
117180
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()
181+
self._use_openweather = use_openweather
182+
self._current_forecast = None
183+
self._data_source = None
184+
self.requests = None
185+
if use_openweather:
186+
self._data_source = _build_openweather_url()
187+
self.requests = adafruit_requests.Session(
188+
pool, ssl.create_default_context()
189+
)
190+
self.get_forecast()
191+
else:
192+
self._current_forecast = self._random_forecast()
193+
194+
@staticmethod
195+
def _random_forecast() -> dict:
196+
"""Generate a plausible BME280-shaped reading."""
197+
return {
198+
"main": {
199+
"temp": round(random.uniform(18.0, 28.0), 2),
200+
"pressure": round(random.uniform(1000.0, 1025.0), 2),
201+
"humidity": round(random.uniform(30.0, 70.0), 2),
202+
}
203+
}
123204

124205
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)
206+
"""Fetch weather from OpenWeatherMap (or generate fake data)."""
207+
if not self._use_openweather:
208+
self._current_forecast = self._random_forecast()
209+
return
210+
response = self.requests.get(self._data_source)
211+
self._current_forecast = response.json()
130212

131213
def _read_temperature(self) -> None:
132214
# Get the OpenWeather temperature and store it in _t_fine
133215
self.get_forecast()
134-
self._t_fine = self._current_forcast["main"]["temp"]
216+
self._t_fine = self._current_forecast["main"]["temp"]
135217

136218
@property
137219
def mode(self) -> int:
@@ -177,7 +259,7 @@ def pressure(self) -> float:
177259
The compensated pressure in hectoPascals.
178260
"""
179261
self._read_temperature()
180-
return self._current_forcast["main"]["pressure"]
262+
return self._current_forecast["main"]["pressure"]
181263

182264
@property
183265
def relative_humidity(self) -> float:
@@ -192,7 +274,7 @@ def humidity(self) -> float:
192274
The relative humidity in RH %
193275
"""
194276
self._read_temperature()
195-
humidity = self._current_forcast["main"]["humidity"]
277+
humidity = self._current_forecast["main"]["humidity"]
196278
if humidity > 100:
197279
return 100
198280
if humidity < 0:
@@ -253,8 +335,13 @@ class Adafruit_BME280_I2C(Adafruit_BME280):
253335
254336
"""
255337

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

259346

260347
class Adafruit_BME280_SPI(Adafruit_BME280):
@@ -305,5 +392,11 @@ class Adafruit_BME280_SPI(Adafruit_BME280):
305392
306393
"""
307394

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

optional_requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
22
#
33
# SPDX-License-Identifier: Unlicense
4+
5+
# Optional: load OpenWeatherMap settings from a .env file on CPython.
6+
python-dotenv

0 commit comments

Comments
 (0)