Skip to content

Shared Module

allsky_shared

allsky_shared.py

Part of allsky postprocess.py modules. https://github.com/AllskyTeam/allsky

This module is a common dumping ground for shared variables and functions used by various Allsky components.

It provides helpers for:

  • Reading environment variables and Allsky configuration
  • Managing small on-disk "databases" used as debug stores
  • Filesystem utilities (paths, permissions, extra-data files)
  • Database connection helpers and automatic purge routines
  • Overlay "extra data" JSON formatting and persistence

convert_lat_lon(input)

Helper for converting latitude/longitude strings.

This wrapper calls the legacy :func:convertLatLon. New code should use this snake_case name; the camelCase function is kept so existing callers continue to work.

See :func:convertLatLon for details.

Source code in scripts/modules/allsky_shared.py
448
449
450
451
452
453
454
455
456
457
458
def convert_lat_lon(input):
    """
    Helper for converting latitude/longitude strings.

    This wrapper calls the legacy :func:`convertLatLon`. New code should
    use this snake_case name; the camelCase function is kept so existing
    callers continue to work.

    See :func:`convertLatLon` for details.
    """
    return convertLatLon(input)

count_starts_in_image(image, mask_file_name=None)

Detect stars in an image using Photutils' DAOStarFinder.

The image is converted to grayscale if needed, optionally masked with :func:mask_image, and then processed with sigma-clipped statistics and DAOStarFinder to locate star centroids.

Parameters:

Name Type Description Default
image ndarray

Input image (grayscale or BGR).

required
mask_file_name str | None

Optional mask file name to apply before detection.

None

Returns:

Type Description

tuple[list[tuple[float, float]], numpy.ndarray]: A tuple containing:

  • A list of (x, y) star coordinates.
  • The (possibly masked) image used for detection.
Source code in scripts/modules/allsky_shared.py
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
def count_starts_in_image(image, mask_file_name=None):
    """
    Detect stars in an image using Photutils' DAOStarFinder.

    The image is converted to grayscale if needed, optionally masked with
    :func:`mask_image`, and then processed with sigma-clipped statistics
    and DAOStarFinder to locate star centroids.

    Args:
        image (numpy.ndarray):
            Input image (grayscale or BGR).
        mask_file_name (str | None, optional):
            Optional mask file name to apply before detection.

    Returns:
        tuple[list[tuple[float, float]], numpy.ndarray]:
            A tuple containing:

              * A list of ``(x, y)`` star coordinates.
              * The (possibly masked) image used for detection.
    """
    from photutils.detection import DAOStarFinder
    from astropy.stats import sigma_clipped_stats
    import cv2

    # Convert to grayscale if it's RGB
    if image.ndim == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image

    if mask_file_name is not None and mask_file_name != '':
        gray = mask_image(gray, mask_file_name)

    # Convert to float for processing
    image_data = gray.astype(float)

    # Estimate background stats
    mean, median, std = sigma_clipped_stats(image_data, sigma=3.0)

    # Detect stars
    daofind = DAOStarFinder(fwhm=3.0, threshold=5.0 * std)
    sources = daofind(image_data - median)

    # Convert to list of (x, y) tuples if sources were found
    coords = []
    if sources is not None and len(sources) > 0:
        x = sources['xcentroid'].tolist()
        y = sources['ycentroid'].tolist()
        coords = list(zip(x, y))

    return coords, image

create_cardinal(degrees)

Convert a wind direction in degrees into a cardinal point.

Parameters:

Name Type Description Default
degrees

Direction in degrees (0–360). North is 0°/360°, east is 90°, and so on.

required

Returns:

Type Description

A string containing one of the 16-point compass directions

(e.g., 'N', 'NE', 'SW'), or 'N/A' if the input

cannot be interpreted.

Source code in scripts/modules/allsky_shared.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def create_cardinal(degrees):
    """
    Convert a wind direction in degrees into a cardinal point.

    Args:
        degrees: Direction in degrees (0–360). North is 0°/360°, east is
            90°, and so on.

    Returns:
        A string containing one of the 16-point compass directions
        (e.g., ``'N'``, ``'NE'``, ``'SW'``), or ``'N/A'`` if the input
        cannot be interpreted.
    """
    try:
        cardinals = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW','W', 'WNW', 'NW', 'NNW', 'N']
        cardinal = cardinals[round(degrees / 22.5)]
    except Exception:
        cardinal = 'N/A'

    return cardinal

create_device(import_name, class_name, bus_number, i2c_address='')

Instantiate an I2C device class on a given bus.

The device class is imported dynamically and initialised with an Adafruit Blinka busio.I2C object constructed from a known set of SCL/SDA pins for the requested bus number.

Parameters:

Name Type Description Default
import_name str

Module path to import (e.g. "adafruit_bme280").

required
class_name str

Name of the device class in that module.

required
bus_number int

I2C bus number (e.g. 1, 3, 4, 5, 6).

required
i2c_address str

Optional I2C address string (e.g. "0x76"). If omitted, the device class is constructed without an explicit address.

''

Returns:

Name Type Description
Any

An instance of the requested device class.

Raises:

Type Description
ImportError

If the module or class cannot be imported.

ValueError

If no pin mapping exists for the given bus number.

Source code in scripts/modules/allsky_shared.py
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
def create_device(import_name: str, class_name: str, bus_number: int, i2c_address: str = ""):
    """
    Instantiate an I2C device class on a given bus.

    The device class is imported dynamically and initialised with an
    Adafruit Blinka ``busio.I2C`` object constructed from a known set of
    SCL/SDA pins for the requested bus number.

    Args:
        import_name (str):
            Module path to import (e.g. ``"adafruit_bme280"``).
        class_name (str):
            Name of the device class in that module.
        bus_number (int):
            I2C bus number (e.g. 1, 3, 4, 5, 6).
        i2c_address (str, optional):
            Optional I2C address string (e.g. ``"0x76"``). If omitted, the
            device class is constructed without an explicit address.

    Returns:
        Any:
            An instance of the requested device class.

    Raises:
        ImportError:
            If the module or class cannot be imported.
        ValueError:
            If no pin mapping exists for the given bus number.
    """
    bus_number = int(bus_number)

    # Define SCL/SDA pins for each bus
    I2C_BUS_PINS = {
        1: (board.SCL, board.SDA),
        3: (board.D5, board.D4),
        4: (board.D9, board.D8),
        5: (board.D13, board.D12),
        6: (board.D23, board.D22)
    }

    # Dynamically import the module and get the class
    try:
        module = importlib.import_module(import_name)
        cls = getattr(module, class_name)
    except (ImportError, AttributeError) as e:
        raise ImportError(f"Could not import '{class_name}' from '{import_name}': {e}")

    try:
        scl, sda = I2C_BUS_PINS[bus_number]
    except KeyError:
        raise ValueError(f"No pin mapping defined for I2C bus {bus_number}")

    i2c = busio.I2C(scl, sda)

    # Instantiate device
    if i2c_address:
        return cls(i2c, int(i2c_address, 0))
    else:
        return cls(i2c)

delete_extra_data(fileName)

Preferred wrapper for removing extra data files.

This is the underscore version and should be used in new code. It simply delegates to :func:deleteExtraData, which contains the legacy implementation.

Parameters:

Name Type Description Default
fileName str

File name to remove from all configured extra data directories.

required
Source code in scripts/modules/allsky_shared.py
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
def delete_extra_data(fileName):
    """
    Preferred wrapper for removing extra data files.

    This is the underscore version and should be used in new code. It simply
    delegates to :func:`deleteExtraData`, which contains the legacy
    implementation.

    Args:
        fileName (str):
            File name to remove from all configured extra data directories.
    """
    deleteExtraData(fileName)

get_all_allsky_variables(show_empty=True, module='', indexed=False, raw=False)

Retrieve all known Allsky variables via the ALLSKYVARIABLES helper.

Parameters:

Name Type Description Default
show_empty bool

Whether to include variables that have no value. Defaults to True.

True
module str

Optional module filter, depending on the ALLSKYVARIABLES implementation.

''
indexed bool

If True, variables may be returned in an indexed form.

False
raw bool

If True, return raw data structures from ALLSKYVARIABLES.

False

Returns:

Name Type Description
Any

Whatever is returned by ALLSKYVARIABLES().get_variables(...).

Source code in scripts/modules/allsky_shared.py
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
def get_all_allsky_variables(show_empty=True, module='', indexed=False, raw=False):
    """
    Retrieve all known Allsky variables via the ALLSKYVARIABLES helper.

    Args:
        show_empty (bool, optional):
            Whether to include variables that have no value. Defaults to True.
        module (str, optional):
            Optional module filter, depending on the ALLSKYVARIABLES
            implementation.
        indexed (bool, optional):
            If True, variables may be returned in an indexed form.
        raw (bool, optional):
            If True, return raw data structures from ALLSKYVARIABLES.

    Returns:
        Any:
            Whatever is returned by ``ALLSKYVARIABLES().get_variables(...)``.
    """
    allskyvariables = ALLSKYVARIABLES()
    return allskyvariables.get_variables(show_empty, module, indexed, raw)

get_allsky_variable(variable)

Look up a single Allsky variable from environment or extra-data files.

The lookup order is:

  1. Environment via :func:getEnvironmentVariable.
  2. JSON extra-data files in all extra-data directories.
  3. Text extra-data files in all extra-data directories.

Parameters:

Name Type Description Default
variable str

The variable name to retrieve.

required

Returns:

Name Type Description
Any

The variable value if found, otherwise None.

Source code in scripts/modules/allsky_shared.py
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
def get_allsky_variable(variable):
    """
    Look up a single Allsky variable from environment or extra-data files.

    The lookup order is:

      1. Environment via :func:`getEnvironmentVariable`.
      2. JSON extra-data files in all extra-data directories.
      3. Text extra-data files in all extra-data directories.

    Args:
        variable (str):
            The variable name to retrieve.

    Returns:
        Any:
            The variable value if found, otherwise None.
    """
    result = getEnvironmentVariable(variable)
    if result is None:

        extra_data_paths = get_extra_dir()
        for extra_data_path in extra_data_paths:
            directory = Path(extra_data_path)

            for file_path in directory.iterdir():
                if file_path.is_file() and isFileReadable(file_path):

                    file_extension = Path(file_path).suffix

                    if file_extension == '.json':
                        result = _get_value_from_json_file(file_path, variable)

                    if file_extension == '.txt':
                        result = _get_value_from_text_file(file_path, variable)

                if result is not None:
                    break

    return result

get_allsky_version()

Convenience helper to retrieve and parse the Allsky version.

The version file path is taken from the ALLSKY_VERSION_FILE environment variable and passed to :func:parse_version.

Returns:

Name Type Description
dict

Parsed version info as returned by :func:parse_version.

Source code in scripts/modules/allsky_shared.py
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
def get_allsky_version():
    """
    Convenience helper to retrieve and parse the Allsky version.

    The version file path is taken from the ``ALLSKY_VERSION_FILE``
    environment variable and passed to :func:`parse_version`.

    Returns:
        dict:
            Parsed version info as returned by :func:`parse_version`.
    """
    version_file = os.environ['ALLSKY_VERSION_FILE']
    version_info = parse_version(version_file)

get_api_url()

Resolve the Allsky API base URL from the environment.

If ALLSKY_API_URL is not present in the current environment, this helper calls :func:setupForCommandLine to load variables from the usual variables.json file, and then re-reads the environment.

Returns:

Name Type Description
str

The API base URL from ALLSKY_API_URL.

Source code in scripts/modules/allsky_shared.py
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
def get_api_url():
    """
    Resolve the Allsky API base URL from the environment.

    If ``ALLSKY_API_URL`` is not present in the current environment, this
    helper calls :func:`setupForCommandLine` to load variables from the
    usual ``variables.json`` file, and then re-reads the environment.

    Returns:
        str:
            The API base URL from ``ALLSKY_API_URL``.
    """
    try:
        api_url = os.environ['ALLSKY_API_URL']
    except KeyError:
        setupForCommandLine()
        api_url = os.environ['ALLSKY_API_URL']

    return api_url

get_camera_gain()

Get the current camera gain.

For Raspberry Pi cameras, this reads the AnalogueGain value from the Pi metadata. For other camera types it uses the AS_GAIN environment variable.

If no gain can be determined, 0.0 is returned.

Returns:

Name Type Description
float

Camera gain value.

Source code in scripts/modules/allsky_shared.py
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
def get_camera_gain():
    """
    Get the current camera gain.

    For Raspberry Pi cameras, this reads the ``AnalogueGain`` value from the
    Pi metadata. For other camera types it uses the ``AS_GAIN`` environment
    variable.

    If no gain can be determined, 0.0 is returned.

    Returns:
        float:
            Camera gain value.
    """
    gain = 0
    camera_type = get_camera_type()

    if camera_type == 'rpi':
        gain = get_rpi_meta_value('AnalogueGain')
    else:
        gain = get_environment_variable('AS_GAIN')

    if gain == None:
        gain = 0

    return float(gain)

get_camera_type()

Get the configured camera type from the environment.

Returns:

Name Type Description
str

Lowercase camera type string (e.g. "rpi").

Source code in scripts/modules/allsky_shared.py
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
def get_camera_type():
    """
    Get the configured camera type from the environment.

    Returns:
        str:
            Lowercase camera type string (e.g. ``"rpi"``).
    """
    camera_type = get_environment_variable('CAMERA_TYPE')
    return camera_type.lower()

get_ecowitt_data(api_key, app_key, mac_address, temp_unitid=1, pressure_unitid=3)

Fetch live weather data from the remote Ecowitt cloud API.

If all of the required credentials are non-empty, the function builds an API URL and attempts to parse a range of fields such as outdoor and indoor temperatures, humidity, rainfall, wind, pressure, and lightning.

All fields are returned in a nested dict with sensible defaults of None.

Parameters:

Name Type Description Default
api_key str

Ecowitt API key.

required
app_key str

Ecowitt application key.

required
mac_address str

Device MAC address registered with Ecowitt.

required
temp_unitid int

Temperature unit ID expected by the API. Defaults to 1.

1
pressure_unitid int

Pressure unit ID expected by the API. Defaults to 3.

3

Returns:

Type Description

dict | str: On success, a nested dictionary of parsed values. On HTTP error, a descriptive error string may be returned instead.

Source code in scripts/modules/allsky_shared.py
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
3625
3626
3627
3628
3629
3630
3631
3632
3633
3634
3635
3636
3637
3638
3639
3640
3641
3642
3643
3644
3645
3646
3647
3648
3649
3650
3651
3652
3653
3654
3655
3656
3657
3658
3659
3660
3661
3662
3663
3664
3665
3666
3667
3668
3669
3670
3671
3672
3673
3674
3675
3676
3677
3678
3679
3680
3681
3682
3683
3684
3685
3686
def get_ecowitt_data(api_key, app_key, mac_address, temp_unitid=1, pressure_unitid=3):
    """
    Fetch live weather data from the remote Ecowitt cloud API.

    If all of the required credentials are non-empty, the function builds
    an API URL and attempts to parse a range of fields such as outdoor and
    indoor temperatures, humidity, rainfall, wind, pressure, and lightning.

    All fields are returned in a nested dict with sensible defaults of None.

    Args:
        api_key (str):
            Ecowitt API key.
        app_key (str):
            Ecowitt application key.
        mac_address (str):
            Device MAC address registered with Ecowitt.
        temp_unitid (int, optional):
            Temperature unit ID expected by the API. Defaults to 1.
        pressure_unitid (int, optional):
            Pressure unit ID expected by the API. Defaults to 3.

    Returns:
        dict | str:
            On success, a nested dictionary of parsed values. On HTTP error,
            a descriptive error string may be returned instead.
    """
    result = {
        'outdoor': {
            'temperature': None,
            'feels_like': None,
            'humidity': None,
            'app_temp': None,
            'dew_point': None
        },
        'indoor': {
            'temperature': None,
            'humidity': None
        },
        'solar_and_uvi': {
            'solar': None,
            'uvi': None
        },
        'rainfall': {
            'rain_rate': None,
            'daily': None,
            'event': None,
            'hourly': None,
            'weekly': None,
            'monthly': None,
            'yearly': None
        },
        'wind': {
            'wind_speed': None,
            'wind_gust': None,
            'wind_direction': None
        },
        'pressure': {
            'relative': None,
            'absolute': None
        },
        'lightning': {
            'distance': None,
            'count': None
        }
    }
    if all(var.strip() for var in (app_key, api_key, mac_address)):
        ECOWITT_API_URL = f'https://api.ecowitt.net/api/v3/device/real_time?application_key={app_key}&api_key={api_key}&mac={mac_address}&call_back=all&temp_unitid={temp_unitid}&pressure_unitid={pressure_unitid}'

        log(4, f"INFO: Reading Ecowitt API from - {ECOWITT_API_URL}")
        try:
            response = requests.get(ECOWITT_API_URL)
            if response.status_code == 200:
                raw_data = response.json()

                result['outdoor']['temperature'] = _get_nested_value(raw_data, 'data.outdoor.temperature.value', float)
                result['outdoor']['feels_like'] = _get_nested_value(raw_data, 'data.outdoor.feels_like.value', float)
                result['outdoor']['humidity'] = _get_nested_value(raw_data, 'data.outdoor.humidity.value', float)
                result['outdoor']['app_temp'] = _get_nested_value(raw_data, 'data.outdoor.app_temp.value', float)
                result['outdoor']['dew_point'] = _get_nested_value(raw_data, 'data.outdoor.dew_point.value', float)

                result['indoor']['temperature'] = _get_nested_value(raw_data, 'data.indoor.temperature.value', float)
                result['indoor']['humidity'] = _get_nested_value(raw_data, 'data.indoor.humidity.value', float)

                result['solar_and_uvi']['solar'] = _get_nested_value(raw_data, 'data.solar_and_uvi.solar.value', float)
                result['solar_and_uvi']['uvi'] = _get_nested_value(raw_data, 'data.solar_and_uvi.uvi.value', float)

                result['rainfall']['rain_rate'] = _get_nested_value(raw_data, 'data.rainfall.rain_rate.value', float)
                result['rainfall']['daily'] = _get_nested_value(raw_data, 'data.rainfall.daily.value', float)
                result['rainfall']['event'] = _get_nested_value(raw_data, 'data.rainfall.event.value', float)
                result['rainfall']['hourly'] = _get_nested_value(raw_data, 'data.rainfall.hourly.value', float)
                result['rainfall']['weekly'] = _get_nested_value(raw_data, 'data.rainfall.weekly.value', float)
                result['rainfall']['monthly'] = _get_nested_value(raw_data, 'data.rainfall.monthly.value', float)
                result['rainfall']['yearly'] = _get_nested_value(raw_data, 'data.rainfall.yearly.value', float)

                result['wind']['wind_speed'] = _get_nested_value(raw_data, 'data.wind.wind_speed.value', float)
                result['wind']['wind_gust'] = _get_nested_value(raw_data, 'data.wind.wind_gust.value', float)
                result['wind']['wind_direction'] = _get_nested_value(raw_data, 'data.wind.wind_direction.value', int)

                result['pressure']['relative'] = _get_nested_value(raw_data, 'data.pressure.relative.value', float)
                result['pressure']['absolute'] = _get_nested_value(raw_data, 'data.pressure.absolute.value', float)

                result['lightning']['distance'] = _get_nested_value(raw_data, 'data.lightning.distance.value', float)
                result['lightning']['count'] = _get_nested_value(raw_data, 'data.lightning.count.value', int)

                log(1, f'INFO: Data read from Ecowitt API')
            else:
                result = f'Got error from the Ecowitt API. Response code {response.status_code}'
                log(0, f'ERROR: {result}')
        except Exception as e:
            me = os.path.basename(__file__)
            eType, eObject, eTraceback = sys.exc_info()
            log(0, f'ERROR: Failed to read data from Ecowitt on line {eTraceback.tb_lineno} in {me} - {e}')
    else:
        log(0, 'ERROR: Missing Ecowitt Application Key, API Key or MAC Address')

    return result

get_ecowitt_local_data(address, password=None)

Fetch live weather data directly from a local Ecowitt gateway.

This variant talks to the gateway's local HTTP API and parses a variety of metrics such as temperatures, humidity, rainfall, wind, pressure and lightning. Values are returned in a nested dict with None defaults.

Units are parsed from the API response and temperature values are converted to Celsius when needed.

Parameters:

Name Type Description Default
address str

Base URL or IP address of the Ecowitt gateway.

required
password str

Reserved for password-protected gateways (currently unused).

None

Returns:

Name Type Description
dict

Nested dictionary of parsed values.

Source code in scripts/modules/allsky_shared.py
3689
3690
3691
3692
3693
3694
3695
3696
3697
3698
3699
3700
3701
3702
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
3718
3719
3720
3721
3722
3723
3724
3725
3726
3727
3728
3729
3730
3731
3732
3733
3734
3735
3736
3737
3738
3739
3740
3741
3742
3743
3744
3745
3746
3747
3748
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
def get_ecowitt_local_data(address, password=None):
    """
    Fetch live weather data directly from a local Ecowitt gateway.

    This variant talks to the gateway's local HTTP API and parses a variety
    of metrics such as temperatures, humidity, rainfall, wind, pressure and
    lightning. Values are returned in a nested dict with None defaults.

    Units are parsed from the API response and temperature values are
    converted to Celsius when needed.

    Args:
        address (str):
            Base URL or IP address of the Ecowitt gateway.
        password (str, optional):
            Reserved for password-protected gateways (currently unused).

    Returns:
        dict:
            Nested dictionary of parsed values.
    """
    '''
    Temp 0 - C, 1 - F
    Pressure 0 - hPA, 1 - inHg, 2 - mmHg
    Wind 0 - m/s, 1 - km/h, 2 - mph, 3 - knots, 5 - Beaufort
    Rain 0 - mm, 1 - in
    Irradiance 0 - Klux, 1 - W/m2, 2 - Kfc
    Capacity 0 - L, 1 - m3, 2 - Gal
    '''

    result = {
        'outdoor': {
            'temperature': None,
            'feels_like': None,
            'humidity': None,
            'app_temp': None,
            'dew_point': None
        },
        'indoor': {
            'temperature': None,
            'humidity': None
        },
        'solar_and_uvi': {
            'solar': None,
            'uvi': None
        },
        'rainfall': {
            'rain_rate': None,
            'daily': None,
            'event': None,
            'hourly': None,
            'weekly': None,
            'monthly': None,
            'yearly': None
        },
        'wind': {
            'wind_speed': None,
            'wind_gust': None,
            'wind_direction': None
        },
        'pressure': {
            'relative': None,
            'absolute': None
        },
        'lightning': {
            'distance': None,
            'count': None
        }
    }

    def parse_val(val, as_type=float, unit=None):
        """
        Extract numeric part from a value string and optionally convert °F to °C.

        Args:
            val (Any):
                Raw value from the Ecowitt JSON (may contain unit text).
            as_type (callable):
                Type to cast the numeric part to (e.g. float, int).
            unit (str | None):
                Optional unit string; when reported as Fahrenheit, values are
                converted to Celsius.

        Returns:
            Any | None:
                Parsed and optionally converted value, or None on failure.
        """
        if val is None:
            return None
        try:
            num_str = str(val).strip().split()[0].strip('%')
            value = as_type(num_str)
            if unit:
                unit = unit.lower()
                if unit in ['f', '°f']:
                    value = round((value - 32) * 5 / 9, 2)
            return value
        except (ValueError, TypeError):
            return None

    def get_val_and_unit(data_list, target_id):
        """
        Helper to locate a record in an Ecowitt list and extract (value, unit).

        Args:
            data_list (list[dict]):
                List of readings as returned by the Ecowitt gateway.
            target_id (str):
                Identifier to match in each dict's ``"id"`` field.

        Returns:
            tuple:
                ``(value, unit)`` where both may be None if the ID is not found.
        """
        for item in data_list:
            if item.get("id") == target_id:
                return item.get("val"), item.get("unit", None)
        return None, None

    LIVE_URL = f'{address}/get_livedata_info?'

    try:
        response = requests.get(LIVE_URL)
        if response.status_code == 200:
            live_data = response.json()

            common = live_data.get("common_list", [])
            val, unit = get_val_and_unit(common, "0x02")
            result['outdoor']['temperature'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x07")
            result['outdoor']['humidity'] = parse_val(val, int, unit)

            val, unit = get_val_and_unit(common, "3")
            result['outdoor']['feels_like'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x03")
            result['outdoor']['dew_point'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x0B")
            result['wind']['wind_speed'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x0C")
            result['wind']['wind_gust'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x0A")
            result['wind']['wind_direction'] = parse_val(val, int, unit)

            val, unit = get_val_and_unit(common, "0x15")
            result['solar_and_uvi']['solar'] = parse_val(val, float, unit)

            val, unit = get_val_and_unit(common, "0x17")
            result['solar_and_uvi']['uvi'] = parse_val(val, int, unit)

            # --- Rain ---
            rain = live_data.get("rain", [])
            for rid, key in {
                "0x0D": "event",
                "0x0E": "rain_rate",
                "0x10": "hourly",
                "0x11": "daily",
                "0x12": "weekly",
                "0x13": "yearly"
            }.items():
                val, unit = get_val_and_unit(rain, rid)
                result['rainfall'][key] = parse_val(val, float, unit)

            # --- WH25 Indoor Sensor ---
            wh25 = live_data.get("wh25", [{}])[0]
            result['indoor']['temperature'] = parse_val(wh25.get("intemp"), float, wh25.get("unit"))
            result['indoor']['humidity'] = parse_val(wh25.get("inhumi"), int)

            result['pressure']['absolute'] = parse_val(wh25.get("abs"), float)
            result['pressure']['relative'] = parse_val(wh25.get("rel"), float)

            # --- Lightning ---
            lightning = live_data.get("lightning", [{}])[0]
            result['lightning']['distance'] = parse_val(lightning.get("distance"), float)
            result['lightning']['count'] = parse_val(lightning.get("count"), int)

    except Exception as e:
        me = os.path.basename(__file__)
        eType, eObject, eTraceback = sys.exc_info()
        log(0, f'ERROR: Failed to read live data from the local Ecowitt gateway on line {eTraceback.tb_lineno} in {me} - {e}')

    return result

get_environment_variable(name, fatal=False, debug=False, try_allsky_debug_file=False)

Helper for reading an environment variable.

This is the modern, snake_case wrapper around the legacy :func:getEnvironmentVariable implementation. New code should call this function rather than the camelCase version.

Parameters:

Name Type Description Default
name

Name of the environment variable to read.

required
fatal

If True and the variable cannot be resolved, the process will terminate with an error.

False
debug

If True, values are read from the debug database instead of the real environment.

False
try_allsky_debug_file

When False (default), the function will fall back to loading variables from variables.json. When True, it will instead try to look up the value in the overlay debug data file.

False

Returns:

Type Description

The resolved value as a string, or None if not found (and fatal

is False).

Source code in scripts/modules/allsky_shared.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def get_environment_variable(name, fatal=False, debug=False, try_allsky_debug_file=False):
    """
    Helper for reading an environment variable.

    This is the modern, snake_case wrapper around the legacy
    :func:`getEnvironmentVariable` implementation. New code should call this
    function rather than the camelCase version.

    Args:
        name: Name of the environment variable to read.
        fatal: If True and the variable cannot be resolved, the process will
            terminate with an error.
        debug: If True, values are read from the debug database instead of
            the real environment.
        try_allsky_debug_file: When False (default), the function will fall
            back to loading variables from ``variables.json``. When True,
            it will instead try to look up the value in the overlay debug
            data file.

    Returns:
        The resolved value as a string, or None if not found (and ``fatal``
        is False).
    """
    return getEnvironmentVariable(name, fatal, debug)

get_extra_dir(current_only=False)

Helper to get the "extra data" directory or directories.

This simply calls :func:getExtraDir. New code should use this snake_case name.

Source code in scripts/modules/allsky_shared.py
1541
1542
1543
1544
1545
1546
1547
1548
def get_extra_dir(current_only:bool = False) -> list[str] | str:
    """
    Helper to get the "extra data" directory or directories.

    This simply calls :func:`getExtraDir`. New code should use this
    snake_case name.
    """
    return getExtraDir(current_only)

get_flows_with_module(module_name)

Scan module flow files and return those containing a given module.

Only postprocessing_*.json files that are not debug variants are considered. Files that fail to parse are quietly ignored.

Parameters:

Name Type Description Default
module_name str

Name of the module to search for in the flow definitions.

required

Returns:

Name Type Description
dict

Mapping of filename to parsed JSON content for flows that contain the given module.

Source code in scripts/modules/allsky_shared.py
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
def get_flows_with_module(module_name):
    """
    Scan module flow files and return those containing a given module.

    Only ``postprocessing_*.json`` files that are not debug variants are
    considered. Files that fail to parse are quietly ignored.

    Args:
        module_name (str):
            Name of the module to search for in the flow definitions.

    Returns:
        dict:
            Mapping of filename to parsed JSON content for flows that contain
            the given module.
    """
    folder = Path(ALLSKY_MODULES)
    found: Dict[str, Any] = {}

    for file in folder.glob("*.json"):
        if not file.name.endswith("-debug.json"):
            if file.name.startswith("postprocessing_"):
                try:
                    with file.open("r", encoding="utf-8") as f:
                        data = json.load(f)

                    if module_name in data:
                        found[file.name] = data

                except (json.JSONDecodeError, OSError) as e:
                    pass

    return found

get_gpio_pin(gpio_pin, pi=None, show_errors=False)

Read the logical state of a GPIO pin via the Allsky API (legacy alias).

This definition simply calls :func:read_gpio_pin. It is kept for backward compatibility with existing code that expects get_gpio_pin to return a pin value rather than a board pin object.

Parameters:

Name Type Description Default
gpio_pin int | str

Pin identifier understood by the Allsky API.

required
pi Any

Unused placeholder kept for interface compatibility.

None
show_errors bool

Currently unused; kept for interface compatibility.

False

Returns:

Name Type Description
bool

True if the GPIO is reported as "on", False otherwise.

Source code in scripts/modules/allsky_shared.py
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
def get_gpio_pin(gpio_pin, pi=None, show_errors=False):
    """
    Read the logical state of a GPIO pin via the Allsky API (legacy alias).

    This definition simply calls :func:`read_gpio_pin`. It is kept for
    backward compatibility with existing code that expects ``get_gpio_pin``
    to return a pin value rather than a board pin object.

    Args:
        gpio_pin (int | str):
            Pin identifier understood by the Allsky API.
        pi (Any, optional):
            Unused placeholder kept for interface compatibility.
        show_errors (bool, optional):
            Currently unused; kept for interface compatibility.

    Returns:
        bool:
            True if the GPIO is reported as ``"on"``, False otherwise.
    """
    return read_gpio_pin(gpio_pin, pi=None, show_errors=False)

get_gpio_pin_details(pin)

Get the CircuitPython board pin object for a given numeric pin.

This is a convenience wrapper around :func:getGPIOPin and should be used by new code.

Parameters:

Name Type Description Default
pin int

Numeric pin index (0–27) corresponding to a board pin.

required

Returns:

Name Type Description
Any

The matching board.Dx constant, or None if the pin is unknown.

Source code in scripts/modules/allsky_shared.py
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
def get_gpio_pin_details(pin):
    """
    Get the CircuitPython ``board`` pin object for a given numeric pin.

    This is a convenience wrapper around :func:`getGPIOPin` and should be
    used by new code.

    Args:
        pin (int):
            Numeric pin index (0–27) corresponding to a board pin.

    Returns:
        Any:
            The matching ``board.Dx`` constant, or None if the pin is unknown.
    """
    return getGPIOPin(pin)

get_hass_sensor_value(ha_url, ha_ltt, ha_sensor)

Query a Home Assistant sensor and return its numeric state.

A GET request is sent to the Home Assistant REST API using the supplied long-lived token. The sensor's state is parsed as a float on success.

Parameters:

Name Type Description Default
ha_url str

Base URL of the Home Assistant instance (e.g. "http://host:8123").

required
ha_ltt str

Long-lived access token for Home Assistant.

required
ha_sensor str

Entity ID of the sensor (e.g. "sensor.outdoor_temp").

required

Returns:

Type Description

float | None: The sensor state as a float, or None if the sensor cannot be read.

Source code in scripts/modules/allsky_shared.py
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
def get_hass_sensor_value(ha_url, ha_ltt, ha_sensor):
    """
    Query a Home Assistant sensor and return its numeric state.

    A GET request is sent to the Home Assistant REST API using the supplied
    long-lived token. The sensor's state is parsed as a float on success.

    Args:
        ha_url (str):
            Base URL of the Home Assistant instance (e.g. ``"http://host:8123"``).
        ha_ltt (str):
            Long-lived access token for Home Assistant.
        ha_sensor (str):
            Entity ID of the sensor (e.g. ``"sensor.outdoor_temp"``).

    Returns:
        float | None:
            The sensor state as a float, or None if the sensor cannot be read.
    """
    result = None

    headers = {
        'Authorization': f'Bearer {ha_ltt}',
        'Content-Type': 'application/json',
    }

    try:
        response = requests.get(f'{ha_url}/api/states/{ha_sensor}', headers=headers)

        if response.status_code == 200:
            result = float(response.json().get('state'))
        else:
            if response.status_code == 404:
                log(0, f'ERROR: Unable to read {ha_sensor} from {ha_url}. homeassistant reports the sensor does not exist')
            else:
                if response.status_code == 401:
                    log(0, f'ERROR: Unable to read {ha_sensor} from {ha_url}. homeassistant reports the token is unauthorised')
                else:
                    log(0, f'ERROR: Unable to read {ha_sensor} from {ha_url}. Error code {response.status_code}')

    except Exception as e:
        me = os.path.basename(__file__)
        eType, eObject, eTraceback = sys.exc_info()
        log(0, f'ERROR: Failed to read data from Homeassistant {eTraceback.tb_lineno} in {me} - {e}')
        result = None

    return result

get_lat_lon()

Read latitude and longitude from settings and return them as floats.

The settings latitude and longitude may be stored in a variety of formats supported by :func:convert_lat_lon (for example, 51.5N or -0.13). If a value is empty, the corresponding return value is None.

Returns:

Type Description

Tuple of (lat, lon) where each element is either a float or

None if not defined.

Source code in scripts/modules/allsky_shared.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
def get_lat_lon():
    """
    Read latitude and longitude from settings and return them as floats.

    The settings ``latitude`` and ``longitude`` may be stored in a variety
    of formats supported by :func:`convert_lat_lon` (for example,
    ``51.5N`` or ``-0.13``). If a value is empty, the corresponding
    return value is ``None``.

    Returns:
        Tuple of ``(lat, lon)`` where each element is either a float or
        None if not defined.
    """
    lat = None
    lon = None

    temp_lat = get_setting('latitude')
    if temp_lat != '':
        lat = convert_lat_lon(temp_lat)
    temp_lon = get_setting('longitude')
    if temp_lon != '':
        lon = convert_lat_lon(temp_lon)

    return lat, lon

get_pi_info(info)

Query simple hardware details about the Raspberry Pi.

Parameters:

Name Type Description Default
info

One of the predefined constants:

  • PI_INFO_MODEL – return the board model string.
  • Pi_INFO_CPU_TEMPERATURE – return the CPU temperature in °C.
required

Returns:

Type Description

The requested value, or None if info does not match a

supported constant.

Source code in scripts/modules/allsky_shared.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
def get_pi_info(info):
    """
    Query simple hardware details about the Raspberry Pi.

    Args:
        info:
            One of the predefined constants:

            - ``PI_INFO_MODEL`` – return the board model string.
            - ``Pi_INFO_CPU_TEMPERATURE`` – return the CPU temperature in °C.

    Returns:
        The requested value, or None if ``info`` does not match a
        supported constant.
    """
    from gpiozero import Device, CPUTemperature
    resukt = None

    if info == PI_INFO_MODEL:
        Device.ensure_pin_factory()
        pi_info = Device.pin_factory.board_info
        result = pi_info.model

    if info == Pi_INFO_CPU_TEMPERATURE:
        result = CPUTemperature().temperature

    return result

get_rpi_meta_value(key)

Read a single value from the Raspberry Pi camera metadata file.

The metadata file format can be either JSON or simple key=value text. This helper tries JSON first and falls back to line-based parsing.

Parameters:

Name Type Description Default
key str

Metadata key to retrieve.

required

Returns:

Name Type Description
Any

The value if found, or 0 if the file is missing, unreadable, or the key is not present.

Source code in scripts/modules/allsky_shared.py
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
def get_rpi_meta_value(key):
    """
    Read a single value from the Raspberry Pi camera metadata file.

    The metadata file format can be either JSON or simple ``key=value`` text.
    This helper tries JSON first and falls back to line-based parsing.

    Args:
        key (str):
            Metadata key to retrieve.

    Returns:
        Any:
            The value if found, or 0 if the file is missing, unreadable, or the
            key is not present.
    """
    result = None
    metadata_path = get_rpi_metadata()

    if metadata_path is not None:
        try:
            with open(metadata_path, 'r') as file:
                metadata = json.load(file)
                if key in metadata:
                    result = metadata[key]
        except json.JSONDecodeError as e:
            with open(metadata_path, 'r') as f:
                for line in f:
                    if line.startswith(key + "="):
                        result = line.split("=", 1)[1].strip()
        except FileNotFoundError as e:
            result = 0
        except Exception as e:
            result = 0

    return result

get_rpi_metadata()

Determine the path to the Raspberry Pi camera metadata file.

The metadata path is extracted from the extraargs in the main settings file if a --metadata argument is present. If no explicit path is found, a default of metadata.txt in the current directory is used.

Returns:

Name Type Description
str

Path to the metadata file.

Source code in scripts/modules/allsky_shared.py
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
def get_rpi_metadata():
    """
    Determine the path to the Raspberry Pi camera metadata file.

    The metadata path is extracted from the ``extraargs`` in the main
    settings file if a ``--metadata`` argument is present. If no explicit
    path is found, a default of ``metadata.txt`` in the current directory
    is used.

    Returns:
        str:
            Path to the metadata file.
    """
    with open(ALLSKY_SETTINGS_FILE) as file:
        config = json.load(file)

    extraargs = config.get('extraargs', '')
    args = shlex.split(extraargs)

    metadata_path = None
    for i, arg in enumerate(args):
        if arg == '--metadata' and i + 1 < len(args):
            metadata_path = args[i + 1]
            break
    if metadata_path is None:
        metadata_path = os.path.join(ALLSKY_CURRENT_DIR, 'metadata.txt')

    return metadata_path

get_secrets(keys)

Load one or more secrets from env.json in ALLSKYPATH.

The file is expected to contain a flat JSON object mapping key names to secret values. Only the requested keys are returned; missing keys are silently ignored.

Parameters:

Name Type Description Default
keys Union[str, List[str]]

Either a single key (string) or a list of key names to fetch.

required

Returns:

Type Description
Union[str, Dict[str, str], None]

If a single key was requested, the value as a string (or None if

Union[str, Dict[str, str], None]

it is not present).

Union[str, Dict[str, str], None]

If multiple keys were requested, a dict mapping each key to

Union[str, Dict[str, str], None]

its value. Keys that are not found are omitted from the mapping.

Union[str, Dict[str, str], None]

If the file cannot be read or parsed, returns None (single key) or

Union[str, Dict[str, str], None]

an empty dict (multiple keys).

Source code in scripts/modules/allsky_shared.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def get_secrets(keys: Union[str, List[str]]) -> Union[str, Dict[str, str], None]:
    """
    Load one or more secrets from ``env.json`` in ``ALLSKYPATH``.

    The file is expected to contain a flat JSON object mapping key names
    to secret values. Only the requested keys are returned; missing keys
    are silently ignored.

    Args:
        keys:
            Either a single key (string) or a list of key names to fetch.

    Returns:
        If a single key was requested, the value as a string (or None if
        it is not present).

        If multiple keys were requested, a ``dict`` mapping each key to
        its value. Keys that are not found are omitted from the mapping.

        If the file cannot be read or parsed, returns None (single key) or
        an empty dict (multiple keys).
    """
    single = isinstance(keys, str)
    if single:
        keys = [keys]

    try:
        file_path = os.path.join(ALLSKYPATH, 'env.json')
        with open(file_path, 'r') as f:
            secrets = json.load(f)

        results = {k: secrets[k] for k in keys if k in secrets}

        if single:
            return results.get(keys[0])
        return results

    except (IOError, json.JSONDecodeError) as e:
        print(f"Error reading secrets file: {e}")
        return None if single else {}

get_sensor_temperature()

Get the current sensor temperature for the active camera.

For Raspberry Pi cameras, this reads the SensorTemperature value from the Pi metadata. For other camera types it uses the AS_TEMPERATURE_C environment variable.

If no temperature can be determined, 0.0 is returned.

Returns:

Name Type Description
float

Sensor temperature in °C.

Source code in scripts/modules/allsky_shared.py
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
def get_sensor_temperature():
    """
    Get the current sensor temperature for the active camera.

    For Raspberry Pi cameras, this reads the ``SensorTemperature`` value
    from the Pi metadata. For other camera types it uses the
    ``AS_TEMPERATURE_C`` environment variable.

    If no temperature can be determined, 0.0 is returned.

    Returns:
        float:
            Sensor temperature in °C.
    """
    temperature = 0
    camera_type = get_camera_type()

    if camera_type == 'rpi':
        temperature = get_rpi_meta_value('SensorTemperature')
    else:
        temperature = get_environment_variable('AS_TEMPERATURE_C')

    if temperature == None:
        temperature = 0

    return float(temperature)

get_setting(settingName)

Helper for reading a setting from the loaded settings JSON.

This wraps the legacy :func:getSetting and should be used in new code. See :func:getSetting for details.

Source code in scripts/modules/allsky_shared.py
728
729
730
731
732
733
734
735
def get_setting(settingName):
    """
    Helper for reading a setting from the loaded settings JSON.

    This wraps the legacy :func:`getSetting` and should be used in new
    code. See :func:`getSetting` for details.
    """
    return getSetting(settingName)

get_value_from_debug_data(key)

Look up a key in the overlay debug file.

This function is used when running in debug mode to retrieve values that would normally be supplied via environment variables.

Parameters:

Name Type Description Default
key str

Name of the value to retrieve.

required

Returns:

Type Description
str | None

The corresponding value as a string with whitespace removed, or

str | None

None if the file does not exist, cannot be read, or the key is

str | None

not present.

Source code in scripts/modules/allsky_shared.py
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
def get_value_from_debug_data(key: str) -> str | None:
    """
    Look up a key in the overlay debug file.

    This function is used when running in debug mode to retrieve values
    that would normally be supplied via environment variables.

    Args:
        key: Name of the value to retrieve.

    Returns:
        The corresponding value as a string with whitespace removed, or
        None if the file does not exist, cannot be read, or the key is
        not present.
    """
    setup_for_command_line()

    try:
        allsky_tmp = os.environ["ALLSKY_TMP"]
        file_path = os.path.join(allsky_tmp, "overlaydebug.txt")

        try:
            with open(file_path, "r", encoding="utf-8") as f:
                for line in f:
                    if not line.strip() or line.strip().startswith("#"):
                        continue

                    parts = line.split(maxsplit=1)
                    if len(parts) == 2 and parts[0] == key:
                        return "".join(parts[1].split())
        except FileNotFoundError:
            return None
    except:
        return None
    return None

load_extra_data_file(file_name, type='')

Load an extra data file from one or more ALLSKY_EXTRA directories.

This helper looks for the given file name in all configured extra-data directories (via :func:get_extra_dir). If it finds a readable file, it will attempt to parse it according to its extension.

Currently only JSON files are parsed. Text files are recognised but not yet processed (the block is a placeholder).

Parameters:

Name Type Description Default
file_name str

Name of the extra data file to load (e.g. "extra.json").

required
type str

Force the file type. If set to "json", the file is parsed as JSON regardless of its extension.

''

Returns:

Name Type Description
dict

Parsed JSON data if successful; otherwise an empty dict.

Source code in scripts/modules/allsky_shared.py
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
def load_extra_data_file(file_name, type=''):
    """
    Load an extra data file from one or more ALLSKY_EXTRA directories.

    This helper looks for the given file name in all configured extra-data
    directories (via :func:`get_extra_dir`). If it finds a readable file,
    it will attempt to parse it according to its extension.

    Currently only JSON files are parsed. Text files are recognised but
    not yet processed (the block is a placeholder).

    Args:
        file_name (str):
            Name of the extra data file to load (e.g. ``"extra.json"``).
        type (str, optional):
            Force the file type. If set to ``"json"``, the file is parsed
            as JSON regardless of its extension.

    Returns:
        dict:
            Parsed JSON data if successful; otherwise an empty dict.
    """
    result = {}
    extra_data_paths = get_extra_dir()
    for extra_data_path in extra_data_paths:
        if extra_data_path is not None:               # it should never be None
            extra_data_filename = os.path.join(extra_data_path, file_name)
            file_path = Path(extra_data_filename)
            if file_path.is_file() and isFileReadable(file_path):
                file_extension = Path(file_path).suffix

                if file_extension == '.json' or type == 'json':
                    try:
                        with open(extra_data_filename, 'r') as file:
                            result = json.load(file)
                    except json.JSONDecodeError:
                        log(0, f'ERROR: cannot read {extra_data_filename}.')

                if file_extension == '.txt':
                    pass

    return result

load_json_file(path)

Load a JSON file and return its parsed contents.

Parameters:

Name Type Description Default
path str | Path

Path to the JSON file.

required

Returns:

Type Description

The parsed JSON data (dict or list). If the file does not exist,

cannot be read, or contains invalid JSON, an empty dict is

returned.

Source code in scripts/modules/allsky_shared.py
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
def load_json_file(path: str | Path):
    """
    Load a JSON file and return its parsed contents.

    Args:
        path: Path to the JSON file.

    Returns:
        The parsed JSON data (dict or list). If the file does not exist,
        cannot be read, or contains invalid JSON, an empty dict is
        returned.
    """
    try:
        file_path = Path(path)
        if not file_path.is_file():
            return {}

        with file_path.open("r", encoding="utf-8") as file:
            return json.load(file)

    except (OSError, json.JSONDecodeError):
        return {}

load_mask(mask_file_name, target_image)

Load a grayscale mask image and resize it to match a target image.

The mask is loaded from ALLSKY_OVERLAY/images/<mask_file_name> and converted to a float mask in the range [0, 1]. If the mask dimensions do not match the target image, it is resized accordingly.

Parameters:

Name Type Description Default
mask_file_name str

Name of the mask file to load.

required
target_image ndarray

Target image whose shape is used for resizing.

required

Returns:

Type Description

numpy.ndarray | None: Float mask array in the range [0, 1], or None if the mask could not be loaded.

Source code in scripts/modules/allsky_shared.py
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
def load_mask(mask_file_name, target_image):
    """
    Load a grayscale mask image and resize it to match a target image.

    The mask is loaded from ``ALLSKY_OVERLAY/images/<mask_file_name>`` and
    converted to a float mask in the range [0, 1]. If the mask dimensions do
    not match the target image, it is resized accordingly.

    Args:
        mask_file_name (str):
            Name of the mask file to load.
        target_image (numpy.ndarray):
            Target image whose shape is used for resizing.

    Returns:
        numpy.ndarray | None:
            Float mask array in the range [0, 1], or None if the mask could
            not be loaded.
    """
    import cv2
    mask = None

    mask_path = os.path.join(ALLSKY_OVERLAY, 'images', mask_file_name)
    target_shape = target_image.shape[:2]

    mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
    if mask is not None:
        if (mask.shape[0] != target_shape[0]) or (mask.shape[1] != target_shape[1]):
            mask = cv2.resize(mask, (target_shape[1], target_shape[0]))
        mask = mask.astype(np.float32) / 255.0

    return mask

load_secrets_file()

Load environment-style secrets from env.json in the Allsky home directory.

Any JSON decoding errors are treated as an empty file.

Returns:

Name Type Description
dict Dict[str, Any]

Parsed secrets dictionary, or an empty dict if missing/invalid.

Source code in scripts/modules/allsky_shared.py
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
def load_secrets_file() -> Dict[str, Any]:
    """
    Load environment-style secrets from ``env.json`` in the Allsky home directory.

    Any JSON decoding errors are treated as an empty file.

    Returns:
        dict:
            Parsed secrets dictionary, or an empty dict if missing/invalid.
    """
    file_path = Path(os.path.join(ALLSKYPATH, 'env.json'))
    env_data: Dict[str, Any] = {}
    if file_path.is_file():
        with file_path.open("r", encoding="utf-8") as f:
            try:
                env_data = json.load(f) or {}
            except json.JSONDecodeError:
                env_data = {}

    return env_data

log(level, text, preventNewline=False, exitCode=None, sendToAllsky=False)

Very simple method to log data if in verbose mode

Log a message to stdout (depending on log level) and optionally forward it to the Allsky WebUI.

Parameters:

Name Type Description Default
level

Numeric log level. The message is printed if the global LOGLEVEL is greater than or equal to this value. Level 0 is treated as an error.

required
text

The message to log.

required
preventNewline

If True, the message is printed without a trailing newline.

False
exitCode

If not None, the process exits with this code after logging the message.

None
sendToAllsky

If True, the message is also passed to the Allsky WebUI via addMessage.sh. Level 0 messages are always sent as errors.

False
Notes

The function does not raise exceptions. If the WebUI message script fails, the error is silently ignored.

Source code in scripts/modules/allsky_shared.py
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
def log(level, text, preventNewline = False, exitCode=None, sendToAllsky=False):
    """ Very simple method to log data if in verbose mode

    Log a message to stdout (depending on log level) and optionally
    forward it to the Allsky WebUI.

    Args:
        level:
            Numeric log level. The message is printed if the global
            ``LOGLEVEL`` is greater than or equal to this value. Level 0
            is treated as an error.
        text:
            The message to log.
        preventNewline:
            If True, the message is printed without a trailing newline.
        exitCode:
            If not None, the process exits with this code after logging
            the message.
        sendToAllsky:
            If True, the message is also passed to the Allsky WebUI via
            ``addMessage.sh``. Level 0 messages are always sent as
            errors.

    Notes:
        The function does not raise exceptions. If the WebUI message
        script fails, the error is silently ignored.
    """
    global LOGLEVEL, ALLSKY_SCRIPTS

    if LOGLEVEL >= level:
        if preventNewline:
            print(text, end="")
        else:
            print(text)

    if sendToAllsky or level == 0:
        if level == 0:
            type = "error"
        else:
            type = "warning"
        # Need to escape single quotes in {text}.
        doubleQuote = '"'
        text = text.replace("'", f"'{doubleQuote}'{doubleQuote}'")
        command = os.path.join(ALLSKY_SCRIPTS, f"addMessage.sh --type {type} --msg '{text}'")
        os.system(command)

    if exitCode is not None:
        sys.exit(exitCode)

mask_image(image, mask_file_name='', log_info=False)

Apply a mask to an image, returning a masked copy.

The mask is loaded via :func:load_mask and applied either directly (for grayscale images) or per-channel (for colour images). The result is clipped and converted back to uint8.

Parameters:

Name Type Description Default
image ndarray

Input image (grayscale or BGR).

required
mask_file_name str

Name of the mask image file. If empty, no masking is performed and None is returned.

''
log_info bool

If True, log a message at level 4 when a mask is applied.

False

Returns:

Type Description

numpy.ndarray | None: Masked image, or None if no mask is applied or an error occurs.

Source code in scripts/modules/allsky_shared.py
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
def mask_image(image, mask_file_name='', log_info=False):
    """
    Apply a mask to an image, returning a masked copy.

    The mask is loaded via :func:`load_mask` and applied either directly
    (for grayscale images) or per-channel (for colour images). The result
    is clipped and converted back to ``uint8``.

    Args:
        image (numpy.ndarray):
            Input image (grayscale or BGR).
        mask_file_name (str, optional):
            Name of the mask image file. If empty, no masking is performed
            and None is returned.
        log_info (bool, optional):
            If True, log a message at level 4 when a mask is applied.

    Returns:
        numpy.ndarray | None:
            Masked image, or None if no mask is applied or an error occurs.
    """
    output = None
    try:
        if mask_file_name != '':
            mask = load_mask(mask_file_name, image)
            if len(image.shape) == 2:
                image = image.astype(np.float32)
                output = image * mask
            else:
                image = image.astype(np.float32)
                if mask.ndim == 2:
                    mask = mask[..., np.newaxis]
                output = image * mask

            output = np.clip(output, 0, 255).astype(np.uint8)

            if log_info:
                log(4, f'INFO: Mask {mask_file_name} applied')

    except Exception as e:
        me = os.path.basename(__file__)
        eType, eObject, eTraceback = sys.exc_info()
        log(0, f'ERROR: mask_image failed on line {eTraceback.tb_lineno} in {me} - {e}')

    return output

normalise_on_off(value)

Normalise an on/off style value to the strings "on" or "off".

Parameters:

Name Type Description Default
value Any

Raw value (e.g. "on", "1", "off", 0).

required

Returns:

Name Type Description
str

"on" if the input looks like an enabled value, otherwise "off".

Source code in scripts/modules/allsky_shared.py
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
def normalise_on_off(value):
    """
    Normalise an on/off style value to the strings ``"on"`` or ``"off"``.

    Args:
        value (Any):
            Raw value (e.g. ``"on"``, ``"1"``, ``"off"``, ``0``).

    Returns:
        str:
            ``"on"`` if the input looks like an enabled value, otherwise ``"off"``.
    """
    if str(value).strip().lower() == 'on' or str(value).strip() == '1':
        return 'on'
    return 'off'

obfuscate_password(password)

Obfuscate a password, leaving the first and last characters visible.

This is used when logging configuration without exposing full credentials.

Parameters:

Name Type Description Default
password str

Original password string.

required

Returns:

Type Description
str

Obfuscated password. Very short passwords are completely masked.

Source code in scripts/modules/allsky_shared.py
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
def obfuscate_password(password: str) -> str:
    """
    Obfuscate a password, leaving the first and last characters visible.

    This is used when logging configuration without exposing full
    credentials.

    Args:
        password: Original password string.

    Returns:
        Obfuscated password. Very short passwords are completely masked.
    """
    if not password:
        return ""
    if len(password) <= 2:
        return "*" * len(password)
    return password[0] + "*" * (len(password) - 2) + password[-1]

read_environment_variable(name)

Read an environment variable without any Allsky-specific fallback.

This is a very thin wrapper around os.environ access and does not attempt to pull values from variables.json or any debug store.

Parameters:

Name Type Description Default
name

Environment variable name.

required

Returns:

Type Description

The value as a string, or None if the variable is not defined.

Source code in scripts/modules/allsky_shared.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def read_environment_variable(name):
    """
    Read an environment variable without any Allsky-specific fallback.

    This is a very thin wrapper around ``os.environ`` access and does not
    attempt to pull values from ``variables.json`` or any debug store.

    Args:
        name: Environment variable name.

    Returns:
        The value as a string, or None if the variable is not defined.
    """
    result = None
    try:
        result = os.environ[name]
    except KeyError:
        result = None        

    return result

read_gpio_pin(gpio_pin, pi=None, show_errors=False)

Read the logical state of a GPIO pin via the Allsky HTTP API.

A GET request is sent to the Allsky API, and the returned JSON is expected to contain a "value" field with the string "on" or "off".

Parameters:

Name Type Description Default
gpio_pin int | str

Pin identifier understood by the Allsky API.

required
pi Any

Unused placeholder for compatibility with other GPIO interfaces.

None
show_errors bool

Currently unused; errors are propagated via exceptions.

False

Returns:

Name Type Description
bool

True if the GPIO value is "on", False otherwise.

Raises:

Type Description
HTTPError

If the HTTP request fails (via raise_for_status).

Source code in scripts/modules/allsky_shared.py
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
def read_gpio_pin(gpio_pin, pi=None, show_errors=False):
    """
    Read the logical state of a GPIO pin via the Allsky HTTP API.

    A GET request is sent to the Allsky API, and the returned JSON is
    expected to contain a ``"value"`` field with the string ``"on"`` or
    ``"off"``.

    Args:
        gpio_pin (int | str):
            Pin identifier understood by the Allsky API.
        pi (Any, optional):
            Unused placeholder for compatibility with other GPIO interfaces.
        show_errors (bool, optional):
            Currently unused; errors are propagated via exceptions.

    Returns:
        bool:
            True if the GPIO value is ``"on"``, False otherwise.

    Raises:
        requests.HTTPError:
            If the HTTP request fails (via ``raise_for_status``).
    """
    api_url = get_api_url()
    response = requests.get(
        f'{api_url}/gpio/digital/{gpio_pin}',
        timeout=2
    )
    response.raise_for_status()
    data = response.json()

    return data.get('value') == 'on'

run_python_script(script, args=None, cwd=None)

Run a Python script using the same interpreter as the current process (e.g., inside a venv).

This function ensures the target script is executed with the current Python interpreter (sys.executable), so that packages installed in the active virtual environment are available.

Parameters:

Name Type Description Default
script str

Path to the Python script to execute.

required
args Optional[List[str]]

Additional arguments to pass to the script. Defaults to None.

None
cwd Optional[str]

Working directory in which to run the script. If None, uses the current directory.

None

Returns:

Type Description
Tuple[int, str]

Tuple[int, str]: A tuple containing: - return code (int): The process's exit code, or 127 if the script is not found. - output (str): Combined standard output and standard error from the script, stripped of trailing whitespace.

Example

code, output = run_python_script("myscript.py", ["--option", "value"]) print(code, output) 0 Script ran successfully

Source code in scripts/modules/allsky_shared.py
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
def run_python_script(script: str, args: Optional[List[str]] = None, cwd: Optional[str] = None) -> Tuple[int, str]:
    """
    Run a Python script using the same interpreter as the current process (e.g., inside a venv).

    This function ensures the target script is executed with the current Python interpreter
    (`sys.executable`), so that packages installed in the active virtual environment are available.

    Args:
        script (str): Path to the Python script to execute.
        args (Optional[List[str]]): Additional arguments to pass to the script. Defaults to None.
        cwd (Optional[str]): Working directory in which to run the script. If None, uses the current directory.

    Returns:
        Tuple[int, str]: A tuple containing:
            - return code (int): The process's exit code, or 127 if the script is not found.
            - output (str): Combined standard output and standard error from the script, stripped of trailing whitespace.

    Example:
        >>> code, output = run_python_script("myscript.py", ["--option", "value"])
        >>> print(code, output)
        0 Script ran successfully
    """
    args = args or []
    try:
        proc = subprocess.run(
            [sys.executable, script, *args],
            capture_output=True,
            text=True,
            check=False,
            cwd=cwd,
        )
        output = (proc.stdout or "") + (proc.stderr or "")
        return proc.returncode, output.strip()
    except FileNotFoundError:
        return 127, f"Script not found: {script}"

run_script(script)

Run an arbitrary executable script and capture its output.

Parameters:

Name Type Description Default
script str

Path to the script or binary to execute.

required

Returns:

Type Description
int

Tuple (returncode, output) where:

str
  • returncode is the process exit code, or 127 if the script was not found.
Tuple[int, str]
  • output is the combined stdout and stderr as a single string.
Source code in scripts/modules/allsky_shared.py
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
def run_script(script: str) -> Tuple[int, str]:
    """
    Run an arbitrary executable script and capture its output.

    Args:
        script: Path to the script or binary to execute.

    Returns:
        Tuple ``(returncode, output)`` where:

        - ``returncode`` is the process exit code, or 127 if the script
          was not found.
        - ``output`` is the combined stdout and stderr as a single
          string.
    """
    try:
        result = subprocess.run(
            [script],
            capture_output=True,
            text=True,
            check=False
        )
        output = result.stdout + result.stderr
        return result.returncode, output.strip()
    except FileNotFoundError:
        return 127, f"Script not found: {script}"

save_extra_data(file_name='', extra_data={}, source='', structure={}, custom_fields={}, event='postcapture')

Persist "extra data" for use by Allsky overlay modules.

This function writes the provided data to a file inside the current ALLSKY_EXTRA directory (resolved via get_extra_dir(True)), using a temporary file in ALLSKY_TMP and an atomic move to avoid partial writes. It ensures the destination directory exists and is web-server accessible, applies final permissions, and (optionally) updates a database when the structure indicates one is in use.

Behavior

1) Ensure extra data directory exists (checkAndCreateDirectory) and enable web access (create_file_web_server_access). 2) If the target filename ends with .json, normalize/shape the payload via format_extra_data_json(extra_data, structure, source). 3) Merge any custom_fields into the payload (overrides existing keys). 4) Serialize to JSON (pretty-printed) and write to a temp file created in ALLSKY_TMP, then atomically move it to the final path. 5) Set mode 0o770 and call set_permissions() for owner/group alignment. 6) If structure contains a "database" key, call update_database().

Parameters:

Name Type Description Default
file_name str

File name (with extension) to write into the extra data directory.

''
extra_data Any

Data to persist. If file_name ends with .json, this should be JSON-serializable. Non-JSON targets are written as the JSON string.

{}
source str

Context or origin tag passed to the JSON formatter. Default: ''.

''
structure dict

Schema/metadata guiding JSON formatting and optional DB updates. If it contains "database", update_database() will be invoked. Default: {}.

{}
custom_fields dict

Extra key/values to inject into the payload before serialization. Keys here override the same keys in extra_data. Default: {}.

{}
event str

Event type (e.g. 'postcapture', 'periodic') used when deciding if database updates should occur.

'postcapture'

Returns:

Type Description

None

Side Effects
  • Creates/updates a file in ALLSKY_EXTRA.
  • Applies filesystem permissions to the output file.
  • May perform a database update if requested by structure.
Error Handling

Any exception is (currently) allowed to propagate only into the surrounding code; earlier versions logged and swallowed errors.

Source code in scripts/modules/allsky_shared.py
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
def save_extra_data(file_name: str = '', extra_data: dict = {}, source: str = '', structure: dict = {}, custom_fields: dict = {}, event: str = 'postcapture'):
    """
    Persist "extra data" for use by Allsky overlay modules.

    This function writes the provided data to a file inside the current
    ALLSKY_EXTRA directory (resolved via `get_extra_dir(True)`), using a
    temporary file in ALLSKY_TMP and an atomic move to avoid partial writes.
    It ensures the destination directory exists and is web-server accessible,
    applies final permissions, and (optionally) updates a database when the
    `structure` indicates one is in use.

    Behavior:
      1) Ensure extra data directory exists (`checkAndCreateDirectory`) and
         enable web access (`create_file_web_server_access`).
      2) If the target filename ends with `.json`, normalize/shape the payload
         via `format_extra_data_json(extra_data, structure, source)`.
      3) Merge any `custom_fields` into the payload (overrides existing keys).
      4) Serialize to JSON (pretty-printed) and write to a temp file created in
         `ALLSKY_TMP`, then atomically move it to the final path.
      5) Set mode 0o770 and call `set_permissions()` for owner/group alignment.
      6) If `structure` contains a `"database"` key, call `update_database()`.

    Args:
        file_name (str):
            File name (with extension) to write into the extra data directory.
        extra_data (Any):
            Data to persist. If `file_name` ends with `.json`, this should be
            JSON-serializable. Non-JSON targets are written as the JSON string.
        source (str, optional):
            Context or origin tag passed to the JSON formatter. Default: ''.
        structure (dict, optional):
            Schema/metadata guiding JSON formatting and optional DB updates.
            If it contains `"database"`, `update_database()` will be invoked.
            Default: {}.
        custom_fields (dict, optional):
            Extra key/values to inject into the payload before serialization.
            Keys here override the same keys in `extra_data`. Default: {}.
        event (str, optional):
            Event type (e.g. ``'postcapture'``, ``'periodic'``) used when
            deciding if database updates should occur.

    Returns:
        None

    Side Effects:
        - Creates/updates a file in ALLSKY_EXTRA.
        - Applies filesystem permissions to the output file.
        - May perform a database update if requested by `structure`.

    Error Handling:
        Any exception is (currently) allowed to propagate only into the
        surrounding code; earlier versions logged and swallowed errors.
    """
    saveExtraData(file_name, extra_data, source, structure, custom_fields, event)

save_json_file(data, filename)

Save a dictionary to a JSON file with pretty formatting.

Parameters:

Name Type Description Default
data dict

Dictionary to save. Must be JSON-serializable.

required
filename Union[str, Path]

Path or string of the file to write.

required

Returns:

Type Description
None

True if the file could be written successfully, False otherwise.

Source code in scripts/modules/allsky_shared.py
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
def save_json_file(data: dict, filename: Union[str, Path]) -> None:
    """
    Save a dictionary to a JSON file with pretty formatting.

    Args:
        data: Dictionary to save. Must be JSON-serializable.
        filename: Path or string of the file to write.

    Returns:
        True if the file could be written successfully, False otherwise.
    """
    file_path = Path(filename)

    try:
        with file_path.open('w', encoding='utf-8') as file:
            json.dump(data, file, ensure_ascii=False, indent=4)
    except:
        return False

    return True

set_gpio_pin(gpio_pin, state, pi=None, show_errors=False)

Set the logical state of a GPIO pin via the Allsky HTTP API.

Parameters:

Name Type Description Default
gpio_pin int | str

Pin identifier understood by the Allsky API.

required
state Any

Desired state; normalised using :func:normalise_on_off to "on" or "off".

required
pi Any

Unused placeholder for compatibility with other GPIO interfaces.

None
show_errors bool

Currently unused; errors are propagated via exceptions.

False

Returns:

Name Type Description
dict

Parsed JSON response from the API.

Raises:

Type Description
HTTPError

If the HTTP request fails (via raise_for_status).

Source code in scripts/modules/allsky_shared.py
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
def set_gpio_pin(gpio_pin, state, pi=None, show_errors=False):
    """
    Set the logical state of a GPIO pin via the Allsky HTTP API.

    Args:
        gpio_pin (int | str):
            Pin identifier understood by the Allsky API.
        state (Any):
            Desired state; normalised using :func:`normalise_on_off` to
            ``"on"`` or ``"off"``.
        pi (Any, optional):
            Unused placeholder for compatibility with other GPIO interfaces.
        show_errors (bool, optional):
            Currently unused; errors are propagated via exceptions.

    Returns:
        dict:
            Parsed JSON response from the API.

    Raises:
        requests.HTTPError:
            If the HTTP request fails (via ``raise_for_status``).
    """
    api_url = get_api_url()
    state = normalise_on_off(state)
    response = requests.post(
        f'{api_url}/gpio/digital',
        json={
            'pin': str(gpio_pin),
            'state': state.lower()
        },
        timeout=2
    )
    response.raise_for_status()
    return response.json()

set_last_run(module)

Helper to record that a module has just run.

This function simply calls the legacy :func:setLastRun. New code should use this name; the camelCase variant is kept for older code.

Source code in scripts/modules/allsky_shared.py
419
420
421
422
423
424
425
426
def set_last_run(module):
    """
    Helper to record that a module has just run.

    This function simply calls the legacy :func:`setLastRun`. New code
    should use this name; the camelCase variant is kept for older code.
    """
    setLastRun(module)

set_pwm(gpio_pin, duty_cycle, pi=None, show_errors=False)

Set PWM output on a GPIO pin via the Allsky HTTP API.

This helper posts the requested duty cycle (and a fixed frequency of 1000Hz) to the API.

Parameters:

Name Type Description Default
gpio_pin int | str

Pin identifier understood by the Allsky API.

required
duty_cycle int | float

Duty cycle value; interpreted by the remote API.

required
pi Any

Unused placeholder for compatibility with other GPIO interfaces.

None
show_errors bool

Currently unused; errors are propagated via exceptions.

False

Returns:

Name Type Description
dict

Parsed JSON response from the API.

Raises:

Type Description
HTTPError

If the HTTP request fails (via raise_for_status).

Source code in scripts/modules/allsky_shared.py
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
def set_pwm(gpio_pin, duty_cycle, pi=None, show_errors=False):
    """
    Set PWM output on a GPIO pin via the Allsky HTTP API.

    This helper posts the requested duty cycle (and a fixed frequency of
    1000Hz) to the API.

    Args:
        gpio_pin (int | str):
            Pin identifier understood by the Allsky API.
        duty_cycle (int | float):
            Duty cycle value; interpreted by the remote API.
        pi (Any, optional):
            Unused placeholder for compatibility with other GPIO interfaces.
        show_errors (bool, optional):
            Currently unused; errors are propagated via exceptions.

    Returns:
        dict:
            Parsed JSON response from the API.

    Raises:
        requests.HTTPError:
            If the HTTP request fails (via ``raise_for_status``).
    """
    api_url = get_api_url()
    frequency = 1000
    response = requests.post(
        f'{api_url}/gpio/pwm',
        json={
            'pin': str(gpio_pin),
            'duty': duty_cycle,
            'frequency': frequency
        },
        timeout=2
    )
    response.raise_for_status()
    return response.json()

should_run(module, period)

Helper to check whether a module should run again.

This wrapper simply calls the legacy :func:shouldRun. New code should use this snake_case name; the camelCase version is retained for backwards compatibility.

See :func:shouldRun for details.

Source code in scripts/modules/allsky_shared.py
372
373
374
375
376
377
378
379
380
381
382
def should_run(module, period):
    """
    Helper to check whether a module should run again.

    This wrapper simply calls the legacy :func:`shouldRun`. New code
    should use this snake_case name; the camelCase version is retained
    for backwards compatibility.

    See :func:`shouldRun` for details.
    """
    return shouldRun(module, period)

stop_pwm(gpio_pin)

Stop PWM output on a GPIO pin via the Allsky HTTP API.

This is implemented by sending a PWM request with 0% duty cycle.

Parameters:

Name Type Description Default
gpio_pin int | str

Pin identifier understood by the Allsky API.

required

Returns:

Name Type Description
dict

Parsed JSON response from the API.

Raises:

Type Description
HTTPError

If the HTTP request fails (via raise_for_status).

Source code in scripts/modules/allsky_shared.py
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
def stop_pwm(gpio_pin):
    """
    Stop PWM output on a GPIO pin via the Allsky HTTP API.

    This is implemented by sending a PWM request with 0% duty cycle.

    Args:
        gpio_pin (int | str):
            Pin identifier understood by the Allsky API.

    Returns:
        dict:
            Parsed JSON response from the API.

    Raises:
        requests.HTTPError:
            If the HTTP request fails (via ``raise_for_status``).
    """
    api_url = get_api_url()
    frequency = 1000
    duty_cycle = 0
    response = requests.post(
        f'{api_url}/gpio/pwm',
        json={
            'pin': str(gpio_pin),
            'duty': duty_cycle,
            'frequency': frequency
        },
        timeout=2
    )
    response.raise_for_status()
    return response.json()

to_bool(v)

Normalise a value to a boolean, supporting several truthy strings.

This version differs slightly from the earlier :func:to_bool in this file: it treats "true", "1", "yes" and "y" (case- insensitive) as True, and everything else as False. It is used when normalising configuration dictionaries.

Parameters:

Name Type Description Default
v bool | str | None

Input value.

required

Returns:

Name Type Description
bool bool

Normalised boolean value.

Source code in scripts/modules/allsky_shared.py
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
def to_bool(v: bool | str) -> bool:
    """
    Normalise a value to a boolean, supporting several truthy strings.

    This version differs slightly from the earlier :func:`to_bool` in this
    file: it treats ``"true"``, ``"1"``, ``"yes"`` and ``"y"`` (case-
    insensitive) as True, and everything else as False. It is used when
    normalising configuration dictionaries.

    Args:
        v (bool | str | None):
            Input value.

    Returns:
        bool:
            Normalised boolean value.
    """
    if isinstance(v, bool):
        return v
    if v is None:
        return False
    return str(v).strip().lower() in ("true", "1", "yes", "y")

update_setting(values)

Helper to update one or more settings.

This wraps the legacy :func:updateSetting. New code should use this snake_case name.

Source code in scripts/modules/allsky_shared.py
799
800
801
802
803
804
805
806
def update_setting(values):
    """
    Helper to update one or more settings.

    This wraps the legacy :func:`updateSetting`. New code should use
    this snake_case name.
    """
    updateSetting(values)