Skip to content

๐Ÿ‡ฒ๐Ÿ‡ช me-caa โ€‹

CountryMontenegro
Registration prefix4O-
Data sourcehttps://www.caa.me/en/registri?field_ispisan_iz_registra_tid=157
FormatHTML table (paginated list, pre-filtered to active registrations only) + per-aircraft HTML detail pages, both at fixed URLs
Run frequencyWeekly (Tuesday, 13:10 UTC)
Depends on Mictronics for ICAO hexYes โ€” this runner does not publish ICAO hex addresses; 4O- registrations are resolved via RediSearch against Mictronics records (idx:aircraft:mictronics). Must run after the mictronics runner.

How it works โ€‹

The registry list URL already includes field_ispisan_iz_registra_tid=157, which restricts results to active registrations, so no deregistration filtering is needed in code โ€” pages are simply paginated until an empty page is returned. For each 4O- registration found, a separate detail page is fetched (by lower-casing the registration into a URL slug) to collect enrichment fields not present on the list page. Detail pages are retried up to twice with exponential backoff on connection errors, timeouts, 403/429, and 5xx responses. Detail-page field extraction tries three strategies in order โ€” Drupal field-label/field-item div pairs, then generic <table> rows, then <dl> definition lists โ€” falling back to the next only if the previous finds nothing. The Category field (e.g. "Transport โ€“ Airplane") is decoded by splitting on the first em-dash or hyphen to derive aircraft.type. The combined Zip code, town field is split via regex into postal_code (leading digits) and city (remainder). Every written record explicitly sets military: false โ€” this register is exclusively civil, and the explicit value ensures a stale military: true flag (from Mictronics or a prior record on a reused hex) is corrected on re-registration.

Columns โ€‹

The list page and each per-aircraft detail page are separate fetches joined on registration (the list page's Registarska oznaka is used as the lookup key, and is also lower-cased into the detail-page URL slug), so columns are enumerated per source below.

List page โ€‹

Source columnImportedNotes
Registarska oznakaโœ…4O-prefix; used as the Mictronics lookup key and as the detail-page join key
Redni broj u registruโŒParsed but not stored
ImeโŒParsed but not stored; superseded by the detail page's Name field
Tipโœ…โ†’ aircraft.model, only if the detail page's Aircraft model/type is empty

Detail page โ€‹

Source columnImportedNotes
AircraftโŒDrupal section-header field; static placeholder text, not per-aircraft data
Manufacturerโœ…โ†’ aircraft.manufacturer
Year Builtโœ…โ†’ aircraft.manufactured_date (YYYY-01-01), only if a 4-digit year
MTOMโŒPresent in source (maximum takeoff mass); not read by this runner
Categoryโœ…โ†’ aircraft.type; decoded from "<category> โ€“ <type>"
S/Nโœ…โ†’ aircraft.serial_number
Aircraft model/typeโœ…โ†’ aircraft.model; preferred over the list page's Tip
ARC expiry dateโŒParsed but not stored
Registration DetailsโŒDrupal section-header field; static placeholder text, not per-aircraft data
Issue DateโŒPresent in source; not read by this runner
RegistrationโŒDuplicate of the list page's Registarska oznaka; the detail page's own copy is never read (registration comes from the list page)
Insert in registerโŒDuplicate of the list page's Redni broj u registru; not read by this runner
DeregโŒPresent in source; not read by this runner (the list URL is already pre-filtered to active registrations)
Operator detailsโŒDrupal section-header field; static placeholder text, not per-aircraft data
Nameโœ…โ†’ registrant.names[0]
Addressโœ…โ†’ registrant.street
Zip code, townโœ…Split into registrant.postal_code and registrant.city
Countryโœ…โ†’ registrant.country
Information about previous registrationโŒDrupal section-header field; static placeholder text, not per-aircraft data
Previous country of registerโŒPresent in source; not read by this runner
Previous registration marksโŒPresent in source; not read by this runner (only present when the aircraft had a prior foreign registration)
Deregistration dateโŒPresent in source; not read by this runner (only present when the aircraft had a prior foreign registration)

See specs/data-dictionary.yaml (me-caa entry) for full column semantics and cross-source schema notes.

Example Output โ€‹

Read back the merged record for a given ICAO hex (combines this runner's data with Mictronics and any other sources that have written to the same key):

bash
docker run --rm --network host redis:latest redis-cli EVAL "$(cat ./shared/lua/merge_aircraft.lua)" 0 516038 | python3 -m json.tool --sort-keys --no-ensure-ascii
json
{
    "aircraft": {
        "manufactured_date": "1980-01-01",
        "manufacturer": "Textron Aviation Inc.",
        "manufacturer_model": "CESSNA 172 Skyhawk",
        "model": "Cessna 172N",
        "serial_number": "70050",
        "type": "Airplane",
        "type_designator": "C172",
        "wake_turbulence_category": "Light"
    },
    "icao_hex": "516038",
    "military": false,
    "registrant": {
        "city": "Podgorica",
        "country": "Montenegro",
        "names": [
            "Airways Scenic & Charter"
        ],
        "postal_code": "81000",
        "street": "Sportski aerodrom ฤ†emovsko Polje"
    },
    "registration": "4O-VUK",
    "source": "me-caa"
}

Montenegro's registry has no Boeing or Airbus airframes, so the larger example below is an Embraer E195 instead of a typical widebody airliner:

bash
docker run --rm --network host redis:latest redis-cli EVAL "$(cat ./shared/lua/merge_aircraft.lua)" 0 516097 | python3 -m json.tool --sort-keys --no-ensure-ascii
json
{
    "aircraft": {
        "manufactured_date": "2009-01-01",
        "manufacturer": "Embraer",
        "manufacturer_model": "EMBRAER ERJ-190-200",
        "model": "ERJ 190-200 LR",
        "serial_number": "19000283",
        "type": "Airplane",
        "type_designator": "E195",
        "wake_turbulence_category": "Medium"
    },
    "icao_hex": "516097",
    "military": false,
    "registrant": {
        "city": "Podgorica",
        "country": "Montenegro",
        "names": [
            "ToMontenegro DOO"
        ],
        "postal_code": "81000",
        "street": "Bulevar Dลพordลพa Vaลกingtona broj 98"
    },
    "registration": "4O-AOB",
    "source": "me-caa"
}

Configuration โ€‹

Reads settings.json (mounted at /app/settings.json):

ParameterRequiredDefaultNotes
redis.hostโœ…โ€”Redis connection host
redis.portโŒ6379
mqtt.hostโŒโ€”Omit the whole mqtt block to skip completion-stats publishing entirely
mqtt.portโŒ1883
mqtt.usernameโŒโ€”Optional MQTT auth; omit for an anonymous broker
mqtt.passwordโŒโ€”
redis_ttl_daysโŒ14TTL applied to each aircraft:registry:{icao_hex} key written by this runner

MQTT โ€‹

Published once, at the end of a run, to SkyFollower/runner/me-caa/statistic/{name} (all retained):

Topic suffixValueFormat
records_importede.g. 271Integer as string
last_run_ate.g. 2026-07-07T14:32:01.123456+00:00ISO 8601 UTC
last_run_statussuccess or failureString

Home Assistant autodiscovery configs are also published (retained) to homeassistant/sensor/SkyFollower_runner_me_caa_{name}/config for each of the three stats above.