Каталка - API погоды для фронта
Версия документа: 1.1.0 Дата: 02.09.2026 Backend API: https://api.katalka.ski
1. Что нужно фронту
Для основной карточки погоды на курорте фронту в первую очередь нужны:
- текущая температура;
- ощущаемая температура;
- погодное состояние / осадки;
- ветер;
- минимальная и максимальная температура на текущий день;
- данные о снеге для курортной погоды;
- время последнего обновления.
Основное поле для выбора погодной иконки — condition.
Backend объединяет два контура данных:
- MET.no — основной погодный прогноз, температура, состояние погоды, ветер, влажность, UV, солнце и базовые осадки.
- Open-Meteo — дополнительный альпийский/снежный контур: снег за последние 24 часа, прогноз снега, глубина снежного покрова и уровень замерзания.
Open-Meteo не вызывается из пользовательского HTTP-запроса для курорта. Его данные заранее обновляются backend-ом и читаются из кэша.
2. Основные endpoint-ы
Погода конкретного курорта — компактная
GET /api/v1/resorts/{resortId}/weather/nearby
Использовать для карточки курорта, Live и компактного погодного блока.
Погода конкретного курорта — подробная
GET /api/v1/resorts/{resortId}/weather/details
Использовать для отдельного экрана погоды, почасового прогноза и прогноза по дням.
Погода по координатам — компактная
GET /api/v1/weather/nearby?lat={latitude}&lng={longitude}
Опционально можно передать высоту:
GET /api/v1/weather/nearby?lat={latitude}&lng={longitude}&altitudeMeters={altitude}
Погода по координатам — подробная
GET /api/v1/weather/details?lat={latitude}&lng={longitude}
Опционально:
GET /api/v1/weather/details?lat={latitude}&lng={longitude}&altitudeMeters={altitude}
В coordinate-based endpoint-ах resortId и resortName будут null — это нормально.
Важно: для координатных endpoint-ов расширенный Open-Meteo cache не используется. Это сделано специально, чтобы не создавать неограниченный кэш для произвольных координат.
3. Компактный ответ
type WeatherNearbyResponse = {
resortId: UUID | null;
resortName: string | null;
latitude: number;
longitude: number;
updatedAt: string;
current: WeatherCurrent | null;
wind: WeatherWind | null;
snow: WeatherSnow | null;
visibility: WeatherVisibility | null;
alpineDataStatus: WeatherDataStatus;
};
Для основной карточки фронту достаточно:
response.current?.temperature
response.current?.feelsLike
response.current?.condition
response.current?.minTemperature
response.current?.maxTemperature
response.wind?.speedMetersPerSecond
response.wind?.gustMetersPerSecond
response.wind?.direction
response.snow
response.updatedAt
4. Текущая температура и состояние
type WeatherCurrent = {
temperature: number | null;
feelsLike: number | null;
minTemperature: number | null;
maxTemperature: number | null;
condition: WeatherCondition;
};
Пример:
{
"temperature": 1.9,
"feelsLike": -1.5,
"minTemperature": 1.1,
"maxTemperature": 1.9,
"condition": "CLOUDY"
}
Для текущего интерфейса:
temperature— основная температура;feelsLike— ощущаемая температура, вычисляется backend-ом;minTemperature— минимум текущего дня;maxTemperature— максимум текущего дня;condition— состояние погоды и ключ для выбора картинки.
5. WeatherCondition
Backend отдаёт собственные стабильные значения:
type WeatherCondition =
| 'CLEAR'
| 'PARTLY_CLOUDY'
| 'CLOUDY'
| 'FOG'
| 'LIGHT_RAIN'
| 'RAIN'
| 'HEAVY_RAIN'
| 'LIGHT_SNOW'
| 'SNOW'
| 'HEAVY_SNOW'
| 'SLEET'
| 'THUNDERSTORM'
| 'UNKNOWN';
Рекомендуемое соответствие:
| condition | Смысл | Иконка |
|---|---|---|
CLEAR | ясно | солнце / ясно |
PARTLY_CLOUDY | частично облачно / fair | солнце + облако |
CLOUDY | облачно | облако |
FOG | туман | туман |
LIGHT_RAIN | слабый дождь | лёгкий дождь |
RAIN | дождь | дождь |
HEAVY_RAIN | сильный дождь | сильный дождь |
LIGHT_SNOW | слабый снег | лёгкий снег |
SNOW | снег | снег |
HEAVY_SNOW | сильный снег | сильный снег |
SLEET | мокрый снег / смешанные осадки | дождь + снег |
THUNDERSTORM | гроза | гроза |
UNKNOWN | провайдер не дал достаточно данных для уверенной классификации | fallback |
Фронту не нужно использовать оригинальные коды MET.no.
Редкий UNKNOWN на дальнем горизонте допустим: это означает, что источник не дал достаточного описания состояния, а не ошибку frontend.
6. Ветер
type WeatherWind = {
speedMetersPerSecond: number | null;
gustMetersPerSecond: number | null;
directionDegrees: number | null;
direction: string | null;
};
Пример:
{
"speedMetersPerSecond": 3.4,
"gustMetersPerSecond": null,
"directionDegrees": 241.7,
"direction": "SW"
}
speedMetersPerSecond— скорость ветра, м/с;gustMetersPerSecond— порывы, м/с;directionDegrees— направление в градусах;direction—N,NE,E,SE,S,SW,W,NW.
Семантика gustMetersPerSecond:
null = провайдер не дал значение порывов
0 = значение получено и порывов нет
> 0 = значение порывов получено
Нельзя трактовать null как 0.
7. Снег и осадки
7.1. Сводка по снегу
type WeatherSnow = {
last24HoursCm: number | null;
forecast24HoursCm: number | null;
condition: SnowCondition;
dataStatus: WeatherDataStatus;
};
type SnowCondition =
| 'NO_SNOW'
| 'POWDER'
| 'FRESH'
| 'WET'
| 'SLUSH'
| 'PACKED'
| 'ICE'
| 'UNKNOWN';
Текущий backend уже уверенно различает отсутствие снежного покрова (NO_SNOW) и отсутствие данных (UNKNOWN).
Пример курортной выдачи:
{
"last24HoursCm": 0.00,
"forecast24HoursCm": 0.00,
"condition": "NO_SNOW",
"dataStatus": "AVAILABLE"
}
7.2. Дождь и снег по часам
В подробном прогнозе:
hourly[].rainMm
hourly[].snowCm
Семантика числовых значений:
0 = данные получены, осадков нет
> 0 = ожидаемое/рассчитанное количество осадков
null = соответствующие данные для этого временного периода не получены
Для rainMm backend использует MET.no. На ближнем часовом горизонте отсутствие дождя нормализуется в 0.
Для snowCm на курортных endpoint-ах backend использует закэшированный Open-Meteo прогноз. На покрытом кэшем горизонте отсутствие снега возвращается как 0.00.
Для картинки осадков всё равно использовать condition, а не только проверку rainMm > 0 или snowCm > 0.
8. Статусы доступности данных
type WeatherDataStatus =
| 'AVAILABLE'
| 'NOT_RECEIVED'
| 'NOT_REQUESTED';
AVAILABLE
Backend получил данные.
Важно:
0 + AVAILABLE
означает достоверное нулевое значение.
Например:
{
"last24HoursCm": 0.00,
"dataStatus": "AVAILABLE"
}
означает: данные получены, за последние 24 часа снега не было.
NOT_RECEIVED
Backend пытался получить/обновить данные, но актуального значения нет.
Например:
{
"last24HoursCm": null,
"forecast24HoursCm": null,
"condition": "UNKNOWN",
"dataStatus": "NOT_RECEIVED"
}
NOT_REQUESTED
Данный внешний контур для такого типа запроса сознательно не вызывается.
Типичный пример — запрос по произвольным координатам:
{
"snow": {
"last24HoursCm": null,
"forecast24HoursCm": null,
"condition": "UNKNOWN",
"dataStatus": "NOT_REQUESTED"
},
"alpineDataStatus": "NOT_REQUESTED"
}
alpineDataStatus
Поле верхнего уровня:
alpineDataStatus: WeatherDataStatus;
Описывает доступность дополнительного Open-Meteo alpine/hourly enrichment:
hourly[].freezingLevelMeters;hourly[].snowCm;- производные
daily[].snowCm.
Для курортного weather при заполненном кэше:
alpineDataStatus = AVAILABLE
Для coordinate-based weather:
alpineDataStatus = NOT_REQUESTED
9. Как работает кэширование и задержки
Это важная часть интеграционного контракта.
9.1. Что происходит в пользовательском запросе
Для курортного endpoint-а схема следующая:
Frontend
|
v
Katalka Backend
|
+--> MET.no — основной прогноз
|
+--> PostgreSQL cache — снежные/alpine данные Open-Meteo
|
v
Response
Open-Meteo не вызывается в request path frontend-а.
Поэтому медленный ответ Open-Meteo, его временная недоступность или timeout не должны заставлять мобильного клиента ждать этот внешний сервис.
9.2. Scheduler Open-Meteo
Backend самостоятельно обновляет Open-Meteo cache:
раз в час, на 10-й минуте часа
Текущая конфигурация:
HH:10
Например:
18:10
19:10
20:10
21:10
...
Scheduler проходит по активным курортам с заданными координатами и обновляет данные Open-Meteo.
Один внешний запрос на курорт получает сразу несколько типов данных:
- snowfall за прошлые 24 часа;
- snowfall forecast;
- snow depth;
- freezing level;
- hourly alpine payload.
9.3. Горизонт Open-Meteo cache
Сейчас запрашивается:
past_hours = 24
forecast_hours = 72
То есть backend хранит:
- примерно 24 часа истории снега;
- примерно 72 часа будущего почасового alpine-прогноза.
Поэтому freezingLevelMeters и snowCm наиболее полно заполнены примерно на ближайшие 3 суток.
На более дальнем MET.no прогнозе эти поля могут снова быть null. Это нормальная граница текущего Open-Meteo cache, а не ошибка.
9.4. Возраст данных
При успешной работе scheduler значение Open-Meteo cache обычно не старше примерно одного часа.
Фактическая задержка зависит от момента запроса:
сразу после HH:10 -> данные только что обновлены
перед следующим HH:10 -> данным около часа
Это сознательный компромисс: снежные и alpine-показатели не требуют обновления на каждый пользовательский запрос.
9.5. Что происходит при ошибке Open-Meteo
Если очередное обновление Open-Meteo не удалось:
- существующий cache не обнуляется;
- последнее успешно полученное значение остаётся в PostgreSQL;
- frontend продолжает получать последнее доступное значение;
- scheduler попробует обновить данные на следующем цикле.
То есть система предпочитает немного устаревшие данные полной недоступности снежного блока.
9.6. Рестарт backend
Open-Meteo cache хранится в PostgreSQL, а не только в памяти Java.
Обычный restart backend не уничтожает кэш:
restart приложения
-> PostgreSQL cache остаётся
-> пользовательские запросы продолжают читать последние данные
Если база/таблица кэша была очищена полностью, до первого успешного scheduler refresh возможны:
snow.dataStatus = NOT_RECEIVED
alpineDataStatus = NOT_RECEIVED
После следующего успешного обновления они снова становятся AVAILABLE.
9.7. MET.no и задержка frontend
MET.no остаётся основным live-provider и вызывается при формировании погодного ответа.
Поэтому:
- основная latency weather endpoint-а зависит от Katalka backend + MET.no;
- latency Open-Meteo из пользовательского запроса исключена;
- дополнительный snow/alpine-блок читается локально из PostgreSQL.
Frontend не должен самостоятельно повторно опрашивать endpoint только ради ожидания Open-Meteo cache.
10. Подробный прогноз
type WeatherDetailsResponse = {
resortId: UUID | null;
resortName: string | null;
latitude: number;
longitude: number;
updatedAt: string;
current: WeatherCurrent | null;
wind: WeatherWind | null;
snow: WeatherSnow | null;
visibility: WeatherVisibility | null;
alpineDataStatus: WeatherDataStatus;
sun: WeatherSun | null;
hourly: WeatherHourly[];
daily: WeatherDaily[];
};
11. Почасовой прогноз
type WeatherHourly = {
datetime: string;
temperature: number | null;
feelsLike: number | null;
humidity: number | null;
uv: number | null;
freezingLevelMeters: number | null;
windSpeedMetersPerSecond: number | null;
windGustMetersPerSecond: number | null;
windDirectionDegrees: number | null;
windDirection: string | null;
condition: WeatherCondition;
snowCm: number | null;
rainMm: number | null;
};
Пример курортной точки ближнего прогноза:
{
"datetime": "2026-09-02T18:00:00Z",
"temperature": 1.9,
"feelsLike": -1.5,
"humidity": 93.8,
"uv": 0.0,
"freezingLevelMeters": 1590.00,
"windSpeedMetersPerSecond": 3.4,
"windGustMetersPerSecond": null,
"windDirectionDegrees": 241.7,
"windDirection": "SW",
"condition": "CLOUDY",
"snowCm": 0.00,
"rainMm": 0
}
Особенности:
freezingLevelMeters— дополнительное Open-Meteo enrichment для курорта;snowCm— Open-Meteo snowfall;rainMm— MET.no precipitation;uvможет отсутствовать на дальнем горизонте;windGustMetersPerSecondможет бытьnull, если MET.no не дал порывы.
12. Прогноз по дням
type WeatherDaily = {
date: string;
minTemperature: number | null;
maxTemperature: number | null;
condition: WeatherCondition;
snowCm: number | null;
};
На датах, покрытых Open-Meteo hourly cache:
{
"date": "2026-09-03",
"minTemperature": 0.6,
"maxTemperature": 7.9,
"condition": "CLEAR",
"snowCm": 0.00
}
После окончания Open-Meteo cache horizon допустимо:
{
"date": "2026-09-07",
"minTemperature": 4.8,
"maxTemperature": 14.8,
"condition": "CLEAR",
"snowCm": null
}
Это означает не «ожидается 0 снега», а «для этой даты snow enrichment не покрыт текущим кэшем».
13. Видимость
type WeatherVisibility = {
meters: number | null;
};
Backend рассчитывает видимость из погодного состояния и доступной информации о тумане.
Пример:
{
"meters": 20000
}
Frontend может переводить в километры:
const visibilityKm =
weather.visibility?.meters != null
? weather.visibility.meters / 1000
: null;
14. Солнце
В подробном ответе:
type WeatherSun = {
sunrise: string | null;
sunset: string | null;
};
Пример:
{
"sunrise": "2026-09-01T23:19:00Z",
"sunset": "2026-09-02T12:55:00Z"
}
Backend отдаёт timestamp. Локальное представление времени — задача frontend-а.
15. Какие null считаются нормальными
Не каждый null означает ошибку.
Нормальные optional null:
wind.gustMetersPerSecond
дальний hourly[].uv
дальний hourly[].freezingLevelMeters
дальний hourly[].snowCm
дальний hourly[].rainMm
дальний daily[].snowCm
редкий дальний WeatherCondition.UNKNOWN
Для snow/alpine данных причину отсутствия нужно определять по:
snow.dataStatus
alpineDataStatus
Не заменять null на 0 на frontend без проверки семантики поля.
16. Рекомендуемый минимальный компонент фронта
const current = weather.current;
const wind = weather.wind;
const viewModel = {
temperature: current?.temperature ?? null,
feelsLike: current?.feelsLike ?? null,
minTemperature: current?.minTemperature ?? null,
maxTemperature: current?.maxTemperature ?? null,
condition: current?.condition ?? 'UNKNOWN',
windSpeed: wind?.speedMetersPerSecond ?? null,
windDirection: wind?.direction ?? null,
snow: weather.snow,
snowDataStatus: weather.snow?.dataStatus ?? 'NOT_RECEIVED',
alpineDataStatus: weather.alpineDataStatus,
updatedAt: weather.updatedAt,
};
Иконку выбирать отдельно:
const icon = weatherIconByCondition[viewModel.condition];
17. Важные правила для frontend
Не определять основное состояние погоды самостоятельно по температуре или количеству осадков.
Основное состояние уже определяет backend через condition.
Правильно:
switch (condition) {
case 'RAIN':
case 'HEAVY_RAIN':
case 'LIGHT_RAIN':
// дождь
break;
case 'SNOW':
case 'HEAVY_SNOW':
case 'LIGHT_SNOW':
// снег
break;
}
Также:
nullи0— разные состояния.gustMetersPerSecond = nullне означает отсутствие порывов.snow.dataStatus = NOT_REQUESTEDдля координатной погоды — штатное поведение.alpineDataStatus = AVAILABLEне означает, что alpine-поля заполнены на все 10 дней: cache horizon сейчас около 72 часов.- Frontend не должен самостоятельно обращаться к Open-Meteo или MET.no — внешний погодный контур агрегирует backend.
18. Кратко для интеграции
Для карточки курорта:
GET /api/v1/resorts/{resortId}/weather/nearby
Использовать:
current.temperature
current.feelsLike
current.condition
current.minTemperature
current.maxTemperature
wind.speedMetersPerSecond
wind.direction
snow
visibility
updatedAt
Для отдельного экрана погоды:
GET /api/v1/resorts/{resortId}/weather/details
Дополнительно использовать:
sun
hourly[]
daily[]
alpineDataStatus
Для произвольных координат:
GET /api/v1/weather/nearby?lat=...&lng=...
GET /api/v1/weather/details?lat=...&lng=...
При известной высоте рекомендуется передавать:
altitudeMeters
Это влияет на температурный прогноз для горной местности.
condition — главный стабильный код погоды для выбора изображения на фронте.
Ключевая семантика snow/alpine данных:
AVAILABLE = данные есть
NOT_RECEIVED = backend ожидал данные, но не получил актуальное значение
NOT_REQUESTED = этот контур для данного запроса сознательно не запрашивается