From b3dd7f0e7a287c332b0c7be609d31cc8a5024dd3 Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Fri, 27 Sep 2024 06:00:34 +0200 Subject: [PATCH 1/6] AS3935 Lightning Detector --- README.md | 7 +- mqtt_io/modules/sensor/as3935.py | 341 +++++++++++++++++++++++++++++++ 2 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 mqtt_io/modules/sensor/as3935.py diff --git a/README.md b/README.md index 03a469c0..15898ce4 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,24 @@ Hardware support is provided by specific GPIO, Sensor and Stream modules. It's e ### Sensors - ADS1x15 analog to digital converters (`ads1x15`) + - ADXl345 3-axis accelerometer up to ±16g (`adxl345`) - AHT20 temperature and humidity sensor (`aht20`) + - AS3935 lightning detector (`as3935`) - BH1750 light level sensor (`bh1750`) - BME280 temperature, humidity and pressure sensor (`bme280`) - BME680 temperature, humidity and pressure sensor (`bme680`) - DHT11/DHT22/AM2302 temperature and humidity sensors (`dht22`) - DS18S20/DS1822/DS18B20/DS1825/DS28EA00/MAX31850K temperature sensors (`ds18b`) - ENS160 digital multi-gas sensor with multiple IAQ data (TVOC, eCO2, AQI) (`ens160`) - - FREQUENCYCOUNTER Counts pulses from GPIOs and return the frequency in Hz (`frequencycounterr`) - FLOWSENSOR generic flow rate sensor like YF-S201, YF-DN50 or others (`flowsensor`) + - FREQUENCYCOUNTER Counts pulses from GPIOs and return the frequency in Hz (`frequencycounterr`) - HCSR04 ultrasonic range sensor (connected to the Raspberry Pi on-board GPIO) (`hcsr04`) - INA219 DC current sensor (`ina219`) - LM75 temperature sensor (`lm75`) - MCP3008 analog to digital converter (`mcp3008`) - - ADXl345 3-axis accelerometer up to ±16g (`adxl345`) - PMS5003 particulate sensor (`pms5003`) - SHT40/SHT41/SHT45 temperature and humidity sensors (`sht4x`) - - TLSl2561 light level sensor (`tsl2561`) + - TSL2561 light level sensor (`tsl2561`) - VEML7700 light level sensor (`veml7700`) - YF-S201 flow rate sensor (`yfs201`) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py new file mode 100644 index 00000000..9f08c970 --- /dev/null +++ b/mqtt_io/modules/sensor/as3935.py @@ -0,0 +1,341 @@ +# pylint: disable=line-too-long +""" + +AS3935 Ligntning Sensor + +Example configuration: + +sensor_modules: + - name: AS3935_Sensor + module: sensor + pin: 17 + auto_filter: True + indoor: True + +sensor_inputs: + - name: distance + module: AS3935_Sensor + digits: 4 + interval: 5 + type: distance· + +Module Options +-------------- +See also: +https://www.embeddedadventures.com/datasheets/AS3935_Datasheet_EN_v2.pdf +https://github.com/fourstix/Sparkfun_CircuitPython_QwiicAS3935/blob/master/sparkfun_qwiicas3935.py +https://github.com/fourstix/Sparkfun_CircuitPython_QwiicAS3935/blob/master/examples + +pin: Interrupt GPIO Pin +auto_filter: Set noise_level, mask_disturber and watchdog_threshold automatically. + Default: True +indoor: Set whether or not the sensor should use an indoor configuration. + Default: True +lightning_threshold: number of lightning detections required before an + interrupt is raised. + Default: 1 +watchdog_threshold: This function returns the threshold for events that trigger the IRQ + Pin. Only sensitivity threshold values 1 to 10 allowed. + Default: 2 +spike_rejection: This setting, like the watchdog threshold, can help determine + between false events and actual lightning. The shape of the spike is + analyzed during the chip's signal validation routine. Increasing this + value increases robustness at the cost of sensitivity to distant + events. + Default: 2 +noise_level: The noise floor level is compared to a known reference voltage. If + this level is exceeded the chip will issue an interrupt to the IRQ pin, + broadcasting that it can not operate properly due to noise (INT_NH). + Check datasheet for specific noise level tolerances when setting this + register. + Default: 2 +mask_disturber Setting this True or False will change whether or not disturbers + trigger the IRQ pin. + Default: False +division_ratio: The antenna is designed to resonate at 500kHz and so can be tuned + with the following setting. The accuracy of the antenna must be within + 3.5 percent of that value for proper signal validation and distance + estimation. The division ratio can only be set to 16, 32, 64 or 128. + Default: 16 +tun_cap: This setting will add capacitance to the series RLC antenna on the + product. It's possible to add 0-120pF in steps of 8pF to the antenna. + The Tuning Cap value will be set between 0 and 120pF, in steps of 8pF. + If necessary, the input value is rounded down to the nearest 8pF. + Default: 0 + +Sensor Options +-------------- + +type: The following types are supported: + last: last lightning in unix timestamp format + distance: distance of last lightning in km + energy: energy of last lightning (no unit, no physical meaning) + number: number of lightning events since start + +""" + +from typing import cast +from ...types import CerberusSchemaType, ConfigType, SensorValueType +from . import GenericSensor +import logging + +_LOG = logging.getLogger(__name__) + +REQUIREMENTS = ("gpiozero","sparkfun_qwiicas3935") + +CONFIG_SCHEMA: CerberusSchemaType = { + "pin": { + "type": 'integer', + "required": True, + "empty": False + }, + "lightning_threshold": { + "type": 'integer', + "required": False, + "empty": False, + "allowed": [1, 5, 9, 16], + "default": 1, + }, + "watchdog_threshold": { + "type": 'integer', + "required": False, + "empty": False, + "min": 1, + "max": 10, + "default": 2, + }, + "spike_rejection": { + "type": 'integer', + "required": False, + "empty": False, + "min": 1, + "max": 11, + "default": 2, + }, + "noise_level": { + "type": 'integer', + "required": False, + "empty": False, + "min": 1, + "max": 7, + "default": 2, + }, + "mask_disturber": { + "type": 'boolean', + "required": False, + "empty": False, + "default": False, + }, + "auto_filter": { + "type": 'boolean', + "required": False, + "empty": False, + "default": True, + }, + "indoor": { + "type": 'boolean', + "required": False, + "empty": False, + "default": True, + }, + "division_ratio": { + "type": 'integer', + "required": False, + "empty": False, + "allowed": [16, 32, 64, 128], + "default": 16, + }, + "tune_cap": { + "type": 'integer', + "required": False, + "empty": False, + "min": 0, + "max": 120, + "default": 0, + }, +} + + +class FRANKLINSENSOR: + """ + Franklin Sensor class + """ + + def __init__(self, gpiozero, lightning, name: str, pin: int) -> None: # type: ignore[no-untyped-def] + self.name = name + self.pin = gpiozero.DigitalInputDevice(pin) + self.pin.when_activated = self.trigger_interrupt + self.count = 0 + self.lightning = lightning + + def trigger_interrupt(self) -> None: + """ When the interrupt goes high """ + # pylint: disable=import-outside-toplevel,attribute-defined-outside-init + # pylint: disable=import-error,no-member + import time # type: ignore + time.sleep(0.05) + global data + _LOG.debug("as3935: Interrupt called!") + interrupt_value = self.lightning.read_interrupt_register() + if interrupt_value == self.lightning.NOISE: + _LOG.debug("as3935: Noise detected.") + if self.lightning.AUTOFILTER is True: + self.reduce_noise() + elif interrupt_value == self.lightning.DISTURBER: + _LOG.debug("as3935: Disturber detected.") + if self.lightning.AUTOFILTER is True: + self.increase_threshold() + elif interrupt_value == self.lightning.LIGHTNING: + _LOG.debug("as3935: Lightning strike detected!") + _LOG.debug("as3935: Approximately: " + str(self.lightning.distance_to_storm) + "km away!") + _LOG.debug("as3935: Energy value: " + str(self.lightning.lightning_energy)) + number = data["number"] + 1 + now = time.time() + data = { + "last": now, + "distance": self.lightning.distance_to_storm, + "energy": self.lightning.lightning_energy, + "number": number + } + + def reduce_noise(self) -> None: + """ Reduce Noise Level """ + value = self.lightning.noise_level + value += 1 + if value > 7: + _LOG.debug("as3935: Noise floor is at the maximum value 7.") + return + _LOG.debug("as3935: Increasing the noise event threshold to " + str(value)) + self.lightning.noise_level = value + + def increase_threshold(self) -> None: + """ Increase Watchdog Threshold """ + value = self.lightning.watchdog_threshold + value += 1 + if value > 10: + self.lightning.mask_disturber = True + _LOG.debug("as3935: Watchdog threshold is at the maximum value 10. Mask disturbers now.") + return + _LOG.debug("as3935: Increasing the disturber watchdog threshold to " + str(value)) + self.lightning.watchdog_threshold = value + +class Sensor(GenericSensor): + """ + Flowsensor: Flow Rate Sensor + """ + + SENSOR_SCHEMA: CerberusSchemaType = { + "type": { + "type": 'string', + "required": False, + "empty": False, + "allowed": ['last', 'distance', 'energy', 'number'], + "default": 'distance', + }, + } + + def setup_module(self) -> None: + # pylint: disable=import-outside-toplevel,attribute-defined-outside-init + # pylint: disable=import-error,no-member + import board # type: ignore + import sparkfun_qwiicas3935 # type: ignore + import gpiozero # type: ignore + + #Create gpio object + self.gpiozero = gpiozero + + # Create bus object using our board's I2C port + self.i2c = board.I2C() + + # Create as3935 object + self.lightning = sparkfun_qwiicas3935.Sparkfun_QwiicAS3935_I2C(self.i2c) + + if self.lightning.connected: + _LOG.debug("as3935: Schmow-ZoW, Lightning Detector Ready!") + else: + _LOG.debug("as3935: Lightning Detector does not appear to be connected. Please check wiring.") + + # Set defaults + self.lightning.clear_statistics() + self.lightning.reset() + self.lightning.watchdog_threshold = 2 + self.lightning.noise_level = 2 + self.lightning.mask_disturber = False + self.lightning.spike_rejection = 2 + self.lightning.lightning_threshold = 1 + self.lightning.division_ratio = 16 + self.lightning.tune_cap = 0 + self.lightning.indoor_outdoor = self.lightning.INDOOR + self.lightning.mask_disturber = False + self.lightning.AUTOFILTER=True # Our own var + + # Auto Filter False-Positives? + if 'auto_filter' in self.config: + self.lightning.AUTOFILTER=self.config["auto_filter"] + + # Lightning Threashold + if 'lightning_threshold' in self.config: + self.lightning.lightning_threshold = self.config["lightning_threshold"] + + # Watchdog Threashold + if 'watchdog_threshold' in self.config: + self.lightning.watchdog_threshold = self.config["watchdog_threshold"] + + # Spike Rejection + if 'spike_rejection' in self.config: + self.lightning.spike_rejection = self.config["spike_rejection"] + + # Noise Floor / Level + if 'noise_level' in self.config: + self.lightning.noise_level = self.config["noise_level"] + + # Mask Disturber + if 'mask_disturber' in self.config: + self.lightning.mask_disturber = self.config["mask_disturber"] + + # Indoor/Outdoor + if 'indoor' in self.config: + if self.config["indoor"] is True: + self.lightning.indoor_outdoor = self.lightning.INDOOR + else: + self.lightning.indoor_outdoor = self.lightning.OUTDOOR + + # Division Ratio + if 'division_ratio' in self.config: + self.lightning.division_ratio = self.config["division_ratio"] + + # Tune Cap + if 'tune_cap' in self.config: + self.lightning.tune_cap = self.config["tune_cap"] + + # Debug + _LOG.debug("as3935: The noise floor is " + str(self.lightning.noise_level)) + _LOG.debug("as3935: The disturber watchdog threshold is " + str(self.lightning.watchdog_threshold)) + _LOG.debug("as3935: The Lightning Detectori's Indoor/Outdoor mode is set to: " + str(self.lightning.indoor_outdoor)) + _LOG.debug("as3935: Are disturbers being masked? " + str(self.lightning.mask_disturber)) + _LOG.debug("as3935: Spike Rejection is set to: " + str(self.lightning.spike_rejection)) + _LOG.debug("as3935: Division Ratio is set to: " + str(self.lightning.division_ratio)) + _LOG.debug("as3935: Internal Capacitor is set to: " + str(self.lightning.tune_cap)) + + # Global data + global data + data = { + "last": 0, + "distance": 0, + "energy": 0, + "number": 0 + } + + # Create sensor + sensor = FRANKLINSENSOR( + gpiozero=self.gpiozero, lightning=self.lightning, name=self.config["name"], pin=self.config["pin"] + ) + + + def get_value(self, sens_conf: ConfigType) -> SensorValueType: + global data + sens_type = sens_conf["type"] + return cast( + float, + data[sens_type] + ) From c8bd6f20187499551c5d55c67b518f1665c41924 Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Fri, 27 Sep 2024 17:35:31 +0200 Subject: [PATCH 2/6] Fix pylint errors --- mqtt_io/modules/sensor/as3935.py | 76 ++++++++++++++++---------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py index 9f08c970..3561ad6a 100644 --- a/mqtt_io/modules/sensor/as3935.py +++ b/mqtt_io/modules/sensor/as3935.py @@ -1,4 +1,6 @@ # pylint: disable=line-too-long +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements """ AS3935 Ligntning Sensor @@ -7,7 +9,7 @@ sensor_modules: - name: AS3935_Sensor - module: sensor + module: as3935 pin: 17 auto_filter: True indoor: True @@ -17,7 +19,7 @@ module: AS3935_Sensor digits: 4 interval: 5 - type: distance· + type: distance Module Options -------------- @@ -74,10 +76,10 @@ """ -from typing import cast +import logging +from typing import Dict from ...types import CerberusSchemaType, ConfigType, SensorValueType from . import GenericSensor -import logging _LOG = logging.getLogger(__name__) @@ -165,8 +167,14 @@ def __init__(self, gpiozero, lightning, name: str, pin: int) -> None: # type: i self.name = name self.pin = gpiozero.DigitalInputDevice(pin) self.pin.when_activated = self.trigger_interrupt - self.count = 0 self.lightning = lightning + self.count = 0 + self.data = { + "last": 0, + "distance": 0, + "energy": 0, + "number": 0 + } def trigger_interrupt(self) -> None: """ When the interrupt goes high """ @@ -174,7 +182,6 @@ def trigger_interrupt(self) -> None: # pylint: disable=import-error,no-member import time # type: ignore time.sleep(0.05) - global data _LOG.debug("as3935: Interrupt called!") interrupt_value = self.lightning.read_interrupt_register() if interrupt_value == self.lightning.NOISE: @@ -187,15 +194,15 @@ def trigger_interrupt(self) -> None: self.increase_threshold() elif interrupt_value == self.lightning.LIGHTNING: _LOG.debug("as3935: Lightning strike detected!") - _LOG.debug("as3935: Approximately: " + str(self.lightning.distance_to_storm) + "km away!") - _LOG.debug("as3935: Energy value: " + str(self.lightning.lightning_energy)) - number = data["number"] + 1 + _LOG.debug("as3935: Approximately: %s km away!", self.lightning.distance_to_storm) + _LOG.debug("as3935: Energy value: %s", self.lightning.lightning_energy) + self.count += 1 now = time.time() - data = { + self.data = { "last": now, "distance": self.lightning.distance_to_storm, "energy": self.lightning.lightning_energy, - "number": number + "number": self.count } def reduce_noise(self) -> None: @@ -205,7 +212,7 @@ def reduce_noise(self) -> None: if value > 7: _LOG.debug("as3935: Noise floor is at the maximum value 7.") return - _LOG.debug("as3935: Increasing the noise event threshold to " + str(value)) + _LOG.debug("as3935: Increasing the noise event threshold to %s", value) self.lightning.noise_level = value def increase_threshold(self) -> None: @@ -216,9 +223,15 @@ def increase_threshold(self) -> None: self.lightning.mask_disturber = True _LOG.debug("as3935: Watchdog threshold is at the maximum value 10. Mask disturbers now.") return - _LOG.debug("as3935: Increasing the disturber watchdog threshold to " + str(value)) + _LOG.debug("as3935: Increasing the disturber watchdog threshold to %s", value) self.lightning.watchdog_threshold = value + def get_value(self, value: str) -> float: + """ Return the value of 'type' """ + value = self.data[value] + return value + + class Sensor(GenericSensor): """ Flowsensor: Flow Rate Sensor @@ -241,8 +254,9 @@ def setup_module(self) -> None: import sparkfun_qwiicas3935 # type: ignore import gpiozero # type: ignore - #Create gpio object + # Create gpio object self.gpiozero = gpiozero + self.sensors: Dict[str, FRANKLINSENSOR] = {} # Create bus object using our board's I2C port self.i2c = board.I2C() @@ -267,7 +281,7 @@ def setup_module(self) -> None: self.lightning.tune_cap = 0 self.lightning.indoor_outdoor = self.lightning.INDOOR self.lightning.mask_disturber = False - self.lightning.AUTOFILTER=True # Our own var + self.lightning.AUTOFILTER = True # Our own var # Auto Filter False-Positives? if 'auto_filter' in self.config: @@ -309,33 +323,21 @@ def setup_module(self) -> None: self.lightning.tune_cap = self.config["tune_cap"] # Debug - _LOG.debug("as3935: The noise floor is " + str(self.lightning.noise_level)) - _LOG.debug("as3935: The disturber watchdog threshold is " + str(self.lightning.watchdog_threshold)) - _LOG.debug("as3935: The Lightning Detectori's Indoor/Outdoor mode is set to: " + str(self.lightning.indoor_outdoor)) - _LOG.debug("as3935: Are disturbers being masked? " + str(self.lightning.mask_disturber)) - _LOG.debug("as3935: Spike Rejection is set to: " + str(self.lightning.spike_rejection)) - _LOG.debug("as3935: Division Ratio is set to: " + str(self.lightning.division_ratio)) - _LOG.debug("as3935: Internal Capacitor is set to: " + str(self.lightning.tune_cap)) - - # Global data - global data - data = { - "last": 0, - "distance": 0, - "energy": 0, - "number": 0 - } + _LOG.debug("as3935: The noise floor is %s", self.lightning.noise_level) + _LOG.debug("as3935: The disturber watchdog threshold is %s", self.lightning.watchdog_threshold) + _LOG.debug("as3935: The Lightning Detectori's Indoor/Outdoor mode is set to: %s", self.lightning.indoor_outdoor) + _LOG.debug("as3935: Are disturbers being masked? %s", self.lightning.mask_disturber) + _LOG.debug("as3935: Spike Rejection is set to: %s", self.lightning.spike_rejection) + _LOG.debug("as3935: Division Ratio is set to: %s", self.lightning.division_ratio) + _LOG.debug("as3935: Internal Capacitor is set to: %s", self.lightning.tune_cap) # Create sensor sensor = FRANKLINSENSOR( gpiozero=self.gpiozero, lightning=self.lightning, name=self.config["name"], pin=self.config["pin"] ) - + self.sensors[sensor.name] = sensor def get_value(self, sens_conf: ConfigType) -> SensorValueType: - global data - sens_type = sens_conf["type"] - return cast( - float, - data[sens_type] + return self.sensors[self.config["name"]].get_value( + sens_conf["type"] ) From 48ab327c3dd6340f622faeb06ea45720372616bf Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Fri, 27 Sep 2024 17:58:03 +0200 Subject: [PATCH 3/6] Fix mypy errors --- mqtt_io/modules/sensor/as3935.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py index 3561ad6a..8b58539a 100644 --- a/mqtt_io/modules/sensor/as3935.py +++ b/mqtt_io/modules/sensor/as3935.py @@ -170,10 +170,10 @@ def __init__(self, gpiozero, lightning, name: str, pin: int) -> None: # type: i self.lightning = lightning self.count = 0 self.data = { - "last": 0, - "distance": 0, - "energy": 0, - "number": 0 + "last": int(0), + "distance": float(0), + "energy": float(0), + "number": int(0) } def trigger_interrupt(self) -> None: @@ -199,10 +199,10 @@ def trigger_interrupt(self) -> None: self.count += 1 now = time.time() self.data = { - "last": now, - "distance": self.lightning.distance_to_storm, - "energy": self.lightning.lightning_energy, - "number": self.count + "last": int(now), + "distance": float(self.lightning.distance_to_storm), + "energy": float(self.lightning.lightning_energy), + "number": int(self.count) } def reduce_noise(self) -> None: @@ -226,9 +226,9 @@ def increase_threshold(self) -> None: _LOG.debug("as3935: Increasing the disturber watchdog threshold to %s", value) self.lightning.watchdog_threshold = value - def get_value(self, value: str) -> float: + def get_value(self, value) -> float: """ Return the value of 'type' """ - value = self.data[value] + value = float(self.data[value]) return value From 27047b30e7e2c625f406b9bcdebe27be466127a9 Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Sat, 28 Sep 2024 17:47:44 +0200 Subject: [PATCH 4/6] Fix mypy errors --- mqtt_io/modules/sensor/as3935.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py index 8b58539a..b4334d7b 100644 --- a/mqtt_io/modules/sensor/as3935.py +++ b/mqtt_io/modules/sensor/as3935.py @@ -228,8 +228,8 @@ def increase_threshold(self) -> None: def get_value(self, value) -> float: """ Return the value of 'type' """ - value = float(self.data[value]) - return value + ret = float(self.data[value]) + return ret class Sensor(GenericSensor): From 650df69f4ae4882a8718f1641b5a552e1add74b8 Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Sat, 28 Sep 2024 17:51:47 +0200 Subject: [PATCH 5/6] Fix mypy errors --- mqtt_io/modules/sensor/as3935.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py index b4334d7b..4b1df94c 100644 --- a/mqtt_io/modules/sensor/as3935.py +++ b/mqtt_io/modules/sensor/as3935.py @@ -226,7 +226,7 @@ def increase_threshold(self) -> None: _LOG.debug("as3935: Increasing the disturber watchdog threshold to %s", value) self.lightning.watchdog_threshold = value - def get_value(self, value) -> float: + def get_value(self, value: str) -> float: """ Return the value of 'type' """ ret = float(self.data[value]) return ret From 4da85faa79a12e59cd2c52780d4a751c2a587c4a Mon Sep 17 00:00:00 2001 From: Michael Schlenstedt Date: Tue, 1 Oct 2024 06:20:39 +0200 Subject: [PATCH 6/6] Bugfix doc --- mqtt_io/modules/sensor/as3935.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mqtt_io/modules/sensor/as3935.py b/mqtt_io/modules/sensor/as3935.py index 4b1df94c..d240c2c9 100644 --- a/mqtt_io/modules/sensor/as3935.py +++ b/mqtt_io/modules/sensor/as3935.py @@ -59,7 +59,7 @@ 3.5 percent of that value for proper signal validation and distance estimation. The division ratio can only be set to 16, 32, 64 or 128. Default: 16 -tun_cap: This setting will add capacitance to the series RLC antenna on the +tune_cap: This setting will add capacitance to the series RLC antenna on the product. It's possible to add 0-120pF in steps of 8pF to the antenna. The Tuning Cap value will be set between 0 and 120pF, in steps of 8pF. If necessary, the input value is rounded down to the nearest 8pF.