From 3f1bb3b32143aab200899c7d33ed8c72dd897860 Mon Sep 17 00:00:00 2001 From: ikeysolomon <134189416+ikeysolomon@users.noreply.github.com> Date: Sat, 21 Mar 2026 11:33:11 +1000 Subject: [PATCH 1/4] Adds FC-37 Rain Sensor functionality. --- indi_allsky/config.py | 4 ++ indi_allsky/constants.py | 5 ++ indi_allsky/devices/sensors/__init__.py | 2 + indi_allsky/devices/sensors/rainSensorFc37.py | 70 +++++++++++++++++++ indi_allsky/flask/forms.py | 8 ++- indi_allsky/flask/templates/config.html | 48 ++++++++++++- indi_allsky/flask/views.py | 3 + indi_allsky/processing.py | 19 +++-- 8 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 indi_allsky/devices/sensors/rainSensorFc37.py diff --git a/indi_allsky/config.py b/indi_allsky/config.py index f2059e0f6..95bbf34f6 100644 --- a/indi_allsky/config.py +++ b/indi_allsky/config.py @@ -767,6 +767,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" : "", @@ -852,6 +853,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 19127fce5..deeefcf72 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)'), ), @@ -4934,6 +4938,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'}) @@ -5274,6 +5279,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') @@ -6048,7 +6054,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_'): @@ -6601,6 +6606,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 10a05ac27..3768b1309 100644 --- a/indi_allsky/flask/templates/config.html +++ b/indi_allsky/flask/templates/config.html @@ -2626,7 +2626,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
@@ -7349,8 +7349,7 @@


Note Sensor units may be adjusted using the Temperature Display option on the Camera Tab
- -
+
@@ -8042,6 +8041,25 @@


+
+
+ {{ 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 }} @@ -10409,6 +10427,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', @@ -11347,6 +11366,29 @@

group_on_ready('FISH2PANO__ENABLE', group_fields_fish2pano, group_checkbox_fields_fish2pano); 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(); + }); diff --git a/indi_allsky/flask/views.py b/indi_allsky/flask/views.py index 484331606..78f6be169 100644 --- a/indi_allsky/flask/views.py +++ b/indi_allsky/flask/views.py @@ -2815,6 +2815,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', ''), @@ -3209,6 +3210,7 @@ def dispatch_request(self): 'MANUAL_GPIO', 'DEVICE', 'TEMP_SENSOR', + 'RAIN_SENSOR', 'THUMBNAILS', 'HEALTHCHECK', 'CHARTS', @@ -3842,6 +3844,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 259663303..6467257a8 100644 --- a/indi_allsky/processing.py +++ b/indi_allsky/processing.py @@ -2759,8 +2759,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' @@ -2921,11 +2920,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' @@ -2970,7 +2981,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 From 4b4f9ad2f2d53aa17b2855b38a8c818225b754ba Mon Sep 17 00:00:00 2001 From: ikeysolomon <134189416+ikeysolomon@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:40:31 +1000 Subject: [PATCH 2/4] fix a formatting issue --- indi_allsky/flask/templates/config.html | 1 + 1 file changed, 1 insertion(+) diff --git a/indi_allsky/flask/templates/config.html b/indi_allsky/flask/templates/config.html index 3768b1309..0f5354205 100644 --- a/indi_allsky/flask/templates/config.html +++ b/indi_allsky/flask/templates/config.html @@ -7350,6 +7350,7 @@

Note Sensor units may be adjusted using the Temperature Display option on the Camera Tab
+
From 70c3cb8220e2f98046f399a693801d8c49602a50 Mon Sep 17 00:00:00 2001 From: ikeysolomon <134189416+ikeysolomon@users.noreply.github.com> Date: Mon, 23 Mar 2026 20:51:15 +1000 Subject: [PATCH 3/4] fix a formatting issue attempt 2 --- indi_allsky/flask/templates/config.html | 1 - 1 file changed, 1 deletion(-) diff --git a/indi_allsky/flask/templates/config.html b/indi_allsky/flask/templates/config.html index 0f5354205..6fb27d7bf 100644 --- a/indi_allsky/flask/templates/config.html +++ b/indi_allsky/flask/templates/config.html @@ -8055,7 +8055,6 @@

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
-

From 72c77044231df344fa257b6979078c7806e1d2ca Mon Sep 17 00:00:00 2001 From: ikeysolomon <134189416+ikeysolomon@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:12:02 +1000 Subject: [PATCH 4/4] updates --- indi_allsky/flask/templates/config.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/indi_allsky/flask/templates/config.html b/indi_allsky/flask/templates/config.html index 882c847a0..751600fac 100644 --- a/indi_allsky/flask/templates/config.html +++ b/indi_allsky/flask/templates/config.html @@ -7398,13 +7398,8 @@


-<<<<<<< HEAD -
Note Sensor units may be adjusted using the Temperature Display option on the Camera Tab
- -=======
Note Sensor units may be adjusted using the Temperature Display option on the Camera Tab
->>>>>>> main
@@ -11453,7 +11448,6 @@

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); -<<<<<<< HEAD // Enable/disable FC-37 Active Low toggle when any sensor slot has FC-37 selected function updateFc37ActiveLowState() { const sensorSlotIds = [ @@ -11477,7 +11471,6 @@

updateFc37ActiveLowState(); }); -======= // Open the camera settings accordion section that matches the selected // CAMERA_INTERFACE value, both on page load and when the selection changes. @@ -11513,7 +11506,6 @@

}); } }); // end $(document).ready ->>>>>>> main