2626 https://circuitpython.org/downloads
2727"""
2828import math
29+ import os
30+ import random
2931import socket as pool
3032import ssl
3133import typing # pylint: disable=unused-import
32- import toml
3334from micropython import const
3435import adafruit_requests
3536from 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+
3748try :
3849 from busio import I2C , SPI
3950 from digitalio import DigitalInOut
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
94146class 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
260346class 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