diff --git a/indi_allsky/config.py b/indi_allsky/config.py index fb34b8563..1d8c9ddbd 100644 --- a/indi_allsky/config.py +++ b/indi_allsky/config.py @@ -770,6 +770,7 @@ class IndiAllSkyConfigBase(object): "F_USER_VAR_SLOT" : "sensor_user_55", "F_I2C_ADDRESS" : "0x52", "F_TITLE_TEMPLATE" : "{name:s} - {label:s} - {probe:s}", + "FC37_ACTIVE_LOW" : True, "OPENWEATHERMAP_APIKEY" : "", "OPENWEATHERMAP_APIKEY_E": "", "WUNDERGROUND_APIKEY" : "", @@ -857,6 +858,9 @@ class IndiAllSkyConfigBase(object): "CUSTOM_SLOT_9" : "sensor_user_18", "CUSTOM_SLOT_9_MIN" : 0.0, }, + "RAIN_SENSOR" : { + "FC37_ACTIVE_LOW" : True, + }, "ADSB" : { "ENABLE" : False, "DUMP1090_URL" : 'https://localhost/dump1090/data/aircraft.json', diff --git a/indi_allsky/constants.py b/indi_allsky/constants.py index 1bc968eb3..6663ee04d 100644 --- a/indi_allsky/constants.py +++ b/indi_allsky/constants.py @@ -193,6 +193,11 @@ SENSOR_USER_CAMERA_SQM_MAG = 8 SENSOR_USER_CAMERA_SQM_ADU = 9 +RAIN_MAP_STR = { + 0 : 'No Rain', + 1 : 'Raining', +} + SENSOR_INDEX_MAP = { 'sensor_user_0' : 0, diff --git a/indi_allsky/devices/sensors/__init__.py b/indi_allsky/devices/sensors/__init__.py index f90f119a9..01ffeef25 100644 --- a/indi_allsky/devices/sensors/__init__.py +++ b/indi_allsky/devices/sensors/__init__.py @@ -79,6 +79,7 @@ from .lightningSensorAs3935 import LightningSensorAs3935_SparkFun_SPI as blinka_sparkfun_lightning_sensor_as3935_spi from .lightningSensorAs3935 import LightningSensorAs3935_SparkFun_I2C as blinka_sparkfun_lightning_sensor_as3935_i2c +from .rainSensorFc37 import RainSensorFc37 as blinka_rain_sensor_fc37 from .mqttBrokerSensor import MqttBrokerSensor as mqtt_broker_sensor @@ -133,6 +134,7 @@ 'kernel_temp_sensor_ds18x20_w1', 'blinka_sparkfun_lightning_sensor_as3935_spi', 'blinka_sparkfun_lightning_sensor_as3935_i2c', + 'blinka_rain_sensor_fc37', 'mqtt_broker_sensor', 'temp_api_openweathermap', 'temp_api_weatherunderground', diff --git a/indi_allsky/devices/sensors/rainSensorFc37.py b/indi_allsky/devices/sensors/rainSensorFc37.py new file mode 100644 index 000000000..e002704aa --- /dev/null +++ b/indi_allsky/devices/sensors/rainSensorFc37.py @@ -0,0 +1,70 @@ +import logging + +from .sensorBase import SensorBase +from ... import constants +from ..exceptions import SensorException + +logger = logging.getLogger('indi_allsky') + + +class RainSensorFc37(SensorBase): + + METADATA = { + 'name': 'FC-37 Rain Sensor', + 'description': 'FC-37 Rain Detection Sensor (digital output)', + 'count': 1, + 'labels': ( + 'Rain Detected', + ), + 'types': ( + constants.SENSOR_PRECIPITATION, + ), + } + + def __init__(self, *args, **kwargs): + super(RainSensorFc37, self).__init__(*args, **kwargs) + + pin_1_name = kwargs.get('pin_1_name') + if not pin_1_name: + raise SensorException('FC-37 sensor pin not configured (RAIN_SENSOR.__*_PIN_1 or TEMP_SENSOR.__*_PIN_1)') + + try: + import board + import digitalio + except Exception as e: + raise SensorException('FC-37 sensor requires board/digitalio support: %s' % str(e)) from e + + if not hasattr(board, pin_1_name): + raise SensorException('FC-37 sensor pin name "%s" is not valid' % pin_1_name) + + self.sensor_pin = digitalio.DigitalInOut(getattr(board, pin_1_name)) + self.sensor_pin.direction = digitalio.Direction.INPUT + self.sensor_pin.pull = digitalio.Pull.UP + + self.active_low = bool( + self.config.get('RAIN_SENSOR', {}).get('FC37_ACTIVE_LOW', True) + ) + + logger.warning('[%s] Initialized FC-37 rain sensor on pin %s, active_low=%s', self.name, pin_1_name, self.active_low) + + def update(self): + try: + raw_value = self.sensor_pin.value + except Exception as e: + raise SensorException('FC-37 sensor read failure: %s' % str(e)) from e + + # FC-37 TFT digital output is typically low when water is detected. + detected = (not raw_value) if self.active_low else raw_value + + rain_value = 1.0 if detected else 0.0 + rain_state = constants.RAIN_MAP_STR.get(int(rain_value), 'Unknown') + + logger.info('[%s] FC-37 rain sensor: %s (%s)', self.name, rain_state, rain_value) + + return {'data': (rain_value,), 'state': rain_state} + + def deinit(self): + try: + self.sensor_pin.deinit() + except Exception: + pass diff --git a/indi_allsky/flask/forms.py b/indi_allsky/flask/forms.py index c4df76e52..1bd735804 100644 --- a/indi_allsky/flask/forms.py +++ b/indi_allsky/flask/forms.py @@ -857,6 +857,7 @@ def IMAGE_LABEL_TEMPLATE_validator(form, field): 'dew_heater_status' : '', 'fan_status' : '', 'wind_dir' : '', + 'rain_sensor' : 'No Rain', 'custom_1' : '', 'custom_2' : '', 'custom_3' : '', @@ -3990,6 +3991,9 @@ class IndiAllskyConfigForm(FlaskForm): ('blinka_sparkfun_lightning_sensor_as3935_spi', 'AS3935 SPI - (6 slots) [BETA]'), ('blinka_sparkfun_lightning_sensor_as3935_i2c', 'AS3935 i2c - (6 slots) [BETA]'), ), + 'Rain Sensors' : ( + ('blinka_rain_sensor_fc37', 'FC-37 Rain Sensor - digital (1 slot)'), + ), 'Remote' : ( ('mqtt_broker_sensor', 'MQTT Broker Sensor - (10 slots)'), ), @@ -4937,6 +4941,7 @@ class IndiAllskyConfigForm(FlaskForm): TEMP_SENSOR__F_USER_VAR_SLOT = SelectField('Sensor F Initial Slot', choices=SENSOR_USER_VAR_SLOT_choices, validators=[SENSOR_USER_VAR_SLOT_validator]) TEMP_SENSOR__F_I2C_ADDRESS = StringField('I2C Address', validators=[DataRequired(), I2C_ADDRESS_validator]) TEMP_SENSOR__F_TITLE_TEMPLATE = StringField('Chart Title Template', validators=[DataRequired(), TEMP_SENSOR__TITLE_TEMPLATE_validator]) + RAIN_SENSOR__FC37_ACTIVE_LOW = BooleanField('Rain Sensor FC-37 -Invert logic') TEMP_SENSOR__OPENWEATHERMAP_APIKEY = PasswordField('OpenWeatherMap API Key', widget=PasswordInput(hide_value=False), validators=[TEMP_SENSOR__OPENWEATHERMAP_APIKEY_validator], render_kw={'autocomplete' : 'new-password'}) TEMP_SENSOR__WUNDERGROUND_APIKEY = PasswordField('Weather Underground API Key', widget=PasswordInput(hide_value=False), validators=[TEMP_SENSOR__WUNDERGROUND_APIKEY_validator], render_kw={'autocomplete' : 'new-password'}) TEMP_SENSOR__ASTROSPHERIC_APIKEY = PasswordField('Astrospheric API Key', widget=PasswordInput(hide_value=False), validators=[TEMP_SENSOR__ASTROSPHERIC_APIKEY_validator], render_kw={'autocomplete' : 'new-password'}) @@ -5279,6 +5284,7 @@ def validate(self): result = False + if self.IMAGE_CROP_ROI_X1.data and self.IMAGE_CROP_ROI_Y1.data and self.IMAGE_CROP_ROI_X2.data and self.IMAGE_CROP_ROI_Y2.data: if self.IMAGE_CROP_ROI_X2.data <= self.IMAGE_CROP_ROI_X1.data: self.IMAGE_CROP_ROI_X2.errors.append('X2 must be greater than X1') @@ -6053,7 +6059,6 @@ def validate(self): self.TEMP_SENSOR__A_PIN_1.errors.append('Topics must be defined') result = False - # sensor B if self.TEMP_SENSOR__B_CLASSNAME.data: if self.TEMP_SENSOR__B_CLASSNAME.data.startswith('blinka_'): @@ -6606,6 +6611,7 @@ def validate(self): }) + for slot1, slot2 in itertools.combinations(check_sensor_slots, 2): if not slot1['set'].isdisjoint(slot2['set']): slot1['slot'].errors.append('Overlapping slots with {0:s}'.format(slot2['name'])) diff --git a/indi_allsky/flask/templates/config.html b/indi_allsky/flask/templates/config.html index 435056242..751600fac 100644 --- a/indi_allsky/flask/templates/config.html +++ b/indi_allsky/flask/templates/config.html @@ -2649,7 +2649,7 @@

Python format syntax
Python datetime format codes
Available variables:
-
timestamp, ts, day_date, exposure, rational_exp, gain_f, temp, temp_unit,
sidereal_time, sqm, stars, detections, stack_method, stack_count, stretch
location, owner, latitude, longitude, kpindex, ovation_max,
sun_alt, moon_alt, moon_phase, sun_moon_sep, moon_up, sun_moon_sep,
mercury_alt, mercury_up, venus_alt, venus_up, venus_phase,
mars_alt, mars_up, jupiter_alt, jupiter_up, saturn_alt, saturn_up,
iss_alt, iss_up, iss_next_h, iss_next_alt, hst_alt, hst_up, hst_next_h,
hst_next_alt, tiangong_alt, tiangong_up, tiangong_next_h, tiangong_next_alt
+
timestamp, ts, day_date, exposure, rational_exp, gain_f, temp, temp_unit,
sidereal_time, sqm, stars, detections, stack_method, stack_count, stretch
location, owner, latitude, longitude, kpindex, ovation_max,
sun_alt, moon_alt, moon_phase, sun_moon_sep, moon_up, sun_moon_sep,
mercury_alt, mercury_up, venus_alt, venus_up, venus_phase,
mars_alt, mars_up, jupiter_alt, jupiter_up, saturn_alt, saturn_up,
iss_alt, iss_up, iss_next_h, iss_next_alt, hst_alt, hst_up, hst_next_h,
hst_next_alt, tiangong_alt, tiangong_up, tiangong_next_h, tiangong_next_alt,
rain_sensor
Image Labels Wiki
@@ -8092,6 +8092,24 @@


+
+
+ {{ form_config.RAIN_SENSOR__FC37_ACTIVE_LOW.label(class='col-form-label') }} +
+
+
+ {{ form_config.RAIN_SENSOR__FC37_ACTIVE_LOW(id='RAIN_SENSOR__FC37_ACTIVE_LOW', class='form-check-input') }} + +
+
+
+
Invert the FC-37 sensor Digital Output logic. Signal default is 0 = No rain, 1 = Rain
+
*Note* In Overlay Template use rain_sensor:s which gives string display
+
+
+ +
+
{{ form_config.TEMP_SENSOR__DHT_USE_PULSEIO.label }} @@ -10488,6 +10506,7 @@

'VIRTUALSKY__SHOWPLANETS', 'VIRTUALSKY__SHOWPLANETLABELS', 'TEMP_SENSOR__DHT_USE_PULSEIO', + 'RAIN_SENSOR__FC37_ACTIVE_LOW', 'TEMP_SENSOR__SHT3X_HEATER_NIGHT', 'TEMP_SENSOR__SHT3X_HEATER_DAY', 'TEMP_SENSOR__HTU31D_HEATER_NIGHT', @@ -11429,6 +11448,29 @@

group_on_ready('IMAGE_OVERLAY__ENABLE', group_fields_image_overlay, group_checkbox_fields_image_overlay); group_on_ready('CIRCULAR_DISPLAY__ENABLE', group_fields_circular_display, group_checkbox_fields_circular_display); + // Enable/disable FC-37 Active Low toggle when any sensor slot has FC-37 selected + function updateFc37ActiveLowState() { + const sensorSlotIds = [ + 'TEMP_SENSOR__A_CLASSNAME', + 'TEMP_SENSOR__B_CLASSNAME', + 'TEMP_SENSOR__C_CLASSNAME', + 'TEMP_SENSOR__D_CLASSNAME', + 'TEMP_SENSOR__E_CLASSNAME', + 'TEMP_SENSOR__F_CLASSNAME', + ]; + const fc37Selected = sensorSlotIds.some(function(id) { + return $('#' + id).val() === 'blinka_rain_sensor_fc37'; + }); + const $toggle = $('#RAIN_SENSOR__FC37_ACTIVE_LOW'); + $toggle.prop('disabled', !fc37Selected); + $toggle.closest('.form-group.row').toggleClass('text-muted', !fc37Selected).css('opacity', fc37Selected ? '' : '0.5'); + } + + $('#TEMP_SENSOR__A_CLASSNAME, #TEMP_SENSOR__B_CLASSNAME, #TEMP_SENSOR__C_CLASSNAME, #TEMP_SENSOR__D_CLASSNAME, #TEMP_SENSOR__E_CLASSNAME, #TEMP_SENSOR__F_CLASSNAME').on('change', updateFc37ActiveLowState); + + updateFc37ActiveLowState(); + +}); // Open the camera settings accordion section that matches the selected // CAMERA_INTERFACE value, both on page load and when the selection changes. diff --git a/indi_allsky/flask/views.py b/indi_allsky/flask/views.py index 1a0f029ca..329d11a2e 100644 --- a/indi_allsky/flask/views.py +++ b/indi_allsky/flask/views.py @@ -2820,6 +2820,7 @@ def get_context(self): 'TEMP_SENSOR__F_I2C_ADDRESS' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('F_I2C_ADDRESS', '0x52'), 'TEMP_SENSOR__F_USER_VAR_SLOT' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('F_USER_VAR_SLOT', 'sensor_user_55'), 'TEMP_SENSOR__F_TITLE_TEMPLATE' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('F_TITLE_TEMPLATE', '{name:s} - {label:s} - {probe:s}'), + 'RAIN_SENSOR__FC37_ACTIVE_LOW' : self.indi_allsky_config.get('RAIN_SENSOR', {}).get('FC37_ACTIVE_LOW', True), 'TEMP_SENSOR__OPENWEATHERMAP_APIKEY' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('OPENWEATHERMAP_APIKEY', ''), 'TEMP_SENSOR__WUNDERGROUND_APIKEY' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('WUNDERGROUND_APIKEY', ''), 'TEMP_SENSOR__ASTROSPHERIC_APIKEY' : self.indi_allsky_config.get('TEMP_SENSOR', {}).get('ASTROSPHERIC_APIKEY', ''), @@ -3216,6 +3217,7 @@ def dispatch_request(self): 'MANUAL_GPIO', 'DEVICE', 'TEMP_SENSOR', + 'RAIN_SENSOR', 'THUMBNAILS', 'HEALTHCHECK', 'CHARTS', @@ -3852,6 +3854,7 @@ def dispatch_request(self): self.indi_allsky_config['TEMP_SENSOR']['F_USER_VAR_SLOT'] = str(request.json['TEMP_SENSOR__F_USER_VAR_SLOT']) self.indi_allsky_config['TEMP_SENSOR']['F_I2C_ADDRESS'] = str(request.json['TEMP_SENSOR__F_I2C_ADDRESS']) self.indi_allsky_config['TEMP_SENSOR']['F_TITLE_TEMPLATE'] = str(request.json['TEMP_SENSOR__F_TITLE_TEMPLATE']) + self.indi_allsky_config['RAIN_SENSOR']['FC37_ACTIVE_LOW'] = bool(request.json.get('RAIN_SENSOR__FC37_ACTIVE_LOW', True)) self.indi_allsky_config['TEMP_SENSOR']['OPENWEATHERMAP_APIKEY'] = str(request.json['TEMP_SENSOR__OPENWEATHERMAP_APIKEY']) self.indi_allsky_config['TEMP_SENSOR']['WUNDERGROUND_APIKEY'] = str(request.json['TEMP_SENSOR__WUNDERGROUND_APIKEY']) self.indi_allsky_config['TEMP_SENSOR']['ASTROSPHERIC_APIKEY'] = str(request.json['TEMP_SENSOR__ASTROSPHERIC_APIKEY']) diff --git a/indi_allsky/processing.py b/indi_allsky/processing.py index e64a544b3..b1200bba2 100644 --- a/indi_allsky/processing.py +++ b/indi_allsky/processing.py @@ -2787,8 +2787,7 @@ def populateSatelliteData(self): def get_image_label(self, i_ref, adsb_aircraft_list, custom_hook_data): # gain is int, gain_f is float - image_label_tmpl = self.config.get('IMAGE_LABEL_TEMPLATE', '{timestamp:%Y%m%d %H:%M:%S}\nExposure {exposure:0.6f}\nGain {gain_f:0.2f}\nTemp {temp:0.1f}{temp_unit:s}\nStars {stars:d}') - + image_label_tmpl = self.config.get('IMAGE_LABEL_TEMPLATE', '{timestamp:%Y%m%d %H:%M:%S}\nExposure {exposure:0.6f}\nGain {gain_f:0.2f}\nTemp {temp:0.1f}{temp_unit:s}\nRain {rain_sensor}\nStars {stars:d}') if self.config.get('TEMP_DISPLAY') == 'f': temp_unit = 'F' @@ -2951,11 +2950,23 @@ def get_image_label(self, i_ref, adsb_aircraft_list, custom_hook_data): # 0 == ccd_temp label_data['temp'] = label_data['sensor_temp_0'] - for x, sensor_data in enumerate(self.sensors_user_av): label_data['sensor_user_{0:d}'.format(x)] = sensor_data + # rain sensor state - scan TEMP_SENSOR A-F slots for FC-37 classname + rain_sensor_label = 'No Rain' + for _slot_letter in ('A', 'B', 'C', 'D', 'E', 'F'): + if self.config.get('TEMP_SENSOR', {}).get('{0:s}_CLASSNAME'.format(_slot_letter)) == 'blinka_rain_sensor_fc37': + _rain_slot_key = self.config.get('TEMP_SENSOR', {}).get('{0:s}_USER_VAR_SLOT'.format(_slot_letter), 'sensor_user_10') + _rain_slot_idx = constants.SENSOR_INDEX_MAP.get(_rain_slot_key) + if _rain_slot_idx is not None: + _rain_state = int(round(self.sensors_user_av[_rain_slot_idx])) + rain_sensor_label = constants.RAIN_MAP_STR.get(_rain_state, 'No Rain') + break + + label_data['rain_sensor'] = rain_sensor_label + # dew heater if self.sensors_user_av[constants.SENSOR_USER_DEW_HEATER_LEVEL]: label_data['dew_heater_status'] = 'On' @@ -3000,7 +3011,7 @@ def get_image_label(self, i_ref, adsb_aircraft_list, custom_hook_data): #label_data['custom_9'] = '' - image_label = image_label_tmpl.format(**label_data) # fill in the data + image_label = image_label_tmpl.format(**label_data) # Add moon mode indicator