2525* Adafruit CircuitPython firmware for the supported boards:
2626 https://circuitpython.org/downloads
2727"""
28+
2829import math
30+ import os
31+ import random
2932import socket as pool
3033import ssl
3134import typing # pylint: disable=unused-import
32- import toml
3335from micropython import const
3436import adafruit_requests
3537from 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+
3749try :
3850 from busio import I2C , SPI
3951 from digitalio import DigitalInOut
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
94147class 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
260347class 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 )
0 commit comments