turkiyem 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  Turkiye Toplu Tasima ve Deprem CLI araci.
4
4
 
5
5
  AFAD deprem verileri, EGO (Ankara) otobus saatleri ve IETT (Istanbul) hat/saat bilgilerini terminalden sorgulayabilirsiniz.
6
+ Open-Meteo ile API key gerektirmeden guncel hava, saatlik tahmin ve hava kalitesi sorgulayabilirsiniz.
6
7
 
7
8
  ## Kurulum
8
9
 
@@ -81,6 +82,30 @@ turkiyem deprem buyukluk 4.0
81
82
 
82
83
  Buyuklugu 4.0 ve ustu olan depremler kirmizi ile vurgulanir.
83
84
 
85
+ ### Hava Durumu ve Hava Kalitesi
86
+
87
+ Open-Meteo uzerinden API key gerektirmeden sorgu yapar:
88
+
89
+ ```bash
90
+ # Secili sehir icin guncel hava
91
+ turkiyem hava guncel
92
+
93
+ # Sehir bazli guncel hava
94
+ turkiyem hava guncel istanbul
95
+
96
+ # Koordinat bazli guncel hava
97
+ turkiyem hava guncel 41.0082,28.9784
98
+
99
+ # Saatlik tahmin (varsayilan 2 gun)
100
+ turkiyem hava saatlik istanbul
101
+
102
+ # Saatlik tahmin gun sayisi (1-7)
103
+ turkiyem hava saatlik istanbul --gun 3
104
+
105
+ # Hava kalitesi
106
+ turkiyem hava kalite ankara
107
+ ```
108
+
84
109
  ### Temizleme
85
110
 
86
111
  Cache ve yapilandirmayi sifirlar:
@@ -107,6 +132,8 @@ Secili sehir `~/.turkiyem/config.json` dosyasinda saklanir. Bu dosya otomatik ol
107
132
  | EGO | Ankara otobus sefer saatleri (ego.gov.tr) |
108
133
  | IETT | Istanbul GTFS hat verileri (data.ibb.gov.tr) |
109
134
  | IETT SOAP | Planlanan sefer saatleri (api.ibb.gov.tr) |
135
+ | Open-Meteo | Guncel hava ve saatlik tahmin (api.open-meteo.com) |
136
+ | Open-Meteo AQ | Hava kalitesi (air-quality-api.open-meteo.com) |
110
137
 
111
138
  ## npm Publish
112
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turkiyem",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Türkiye Toplu Taşıma ve Deprem CLI aracı - AFAD deprem verileri, EGO hat saatleri ve IETT SOAP/GTFS bilgileri",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -21,7 +21,10 @@
21
21
  "cli",
22
22
  "earthquake",
23
23
  "transit",
24
- "soap"
24
+ "soap",
25
+ "weather",
26
+ "open-meteo",
27
+ "air-quality"
25
28
  ],
26
29
  "author": "",
27
30
  "license": "MIT",
@@ -0,0 +1,58 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchAirQuality, fetchCurrentWeather, fetchHourlyForecast } from '../services/weatherService.js';
4
+ import {
5
+ createAirQualityTable,
6
+ createCurrentWeatherTable,
7
+ createHourlyWeatherTable,
8
+ } from '../utils/display.js';
9
+
10
+ function formatLocationHint(city) {
11
+ if (city) return city;
12
+ return 'seçili şehir';
13
+ }
14
+
15
+ export async function havaGuncel(city) {
16
+ const spinner = ora(`Güncel hava verisi alınıyor (${formatLocationHint(city)})...`).start();
17
+
18
+ try {
19
+ const result = await fetchCurrentWeather(city);
20
+ spinner.succeed('Güncel hava verisi alındı');
21
+ console.log('');
22
+ console.log(createCurrentWeatherTable(result));
23
+ } catch (err) {
24
+ spinner.fail(chalk.red(err.message));
25
+ }
26
+ }
27
+
28
+ export async function havaSaatlik(city, days) {
29
+ const parsedDays = Number.parseInt(days, 10);
30
+ if (!Number.isNaN(parsedDays) && (parsedDays < 1 || parsedDays > 7)) {
31
+ console.log(chalk.red('Gün sayısı 1 ile 7 arasında olmalıdır.'));
32
+ return;
33
+ }
34
+
35
+ const spinner = ora(`Saatlik tahmin verisi alınıyor (${formatLocationHint(city)})...`).start();
36
+
37
+ try {
38
+ const result = await fetchHourlyForecast(city, parsedDays || 2);
39
+ spinner.succeed(`Saatlik tahmin verisi alındı (${result.forecastDays} gün)`);
40
+ console.log('');
41
+ console.log(createHourlyWeatherTable(result));
42
+ } catch (err) {
43
+ spinner.fail(chalk.red(err.message));
44
+ }
45
+ }
46
+
47
+ export async function havaKalitesi(city) {
48
+ const spinner = ora(`Hava kalitesi verisi alınıyor (${formatLocationHint(city)})...`).start();
49
+
50
+ try {
51
+ const result = await fetchAirQuality(city);
52
+ spinner.succeed('Hava kalitesi verisi alındı');
53
+ console.log('');
54
+ console.log(createAirQualityTable(result));
55
+ } catch (err) {
56
+ spinner.fail(chalk.red(err.message));
57
+ }
58
+ }
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ import { printBanner, printHelp } from './utils/banner.js';
7
7
  import { sehirSec } from './commands/sehir.js';
8
8
  import { hatSorgula } from './commands/hat.js';
9
9
  import { depremSon24, deprem7Gun, depremBuyukluk } from './commands/deprem.js';
10
+ import { havaGuncel, havaKalitesi, havaSaatlik } from './commands/hava.js';
10
11
  import { temizle } from './commands/temizle.js';
11
12
 
12
13
  const require = createRequire(import.meta.url);
@@ -70,6 +71,32 @@ depremCmd
70
71
  await depremBuyukluk(deger);
71
72
  });
72
73
 
74
+ const havaCmd = program
75
+ .command('hava')
76
+ .description('Hava durumu ve hava kalitesi sorgula');
77
+
78
+ havaCmd
79
+ .command('guncel [sehirVeyaKoordinat]')
80
+ .description('Güncel sıcaklık, rüzgar ve nem bilgisi')
81
+ .action(async (sehirVeyaKoordinat) => {
82
+ await havaGuncel(sehirVeyaKoordinat);
83
+ });
84
+
85
+ havaCmd
86
+ .command('saatlik [sehirVeyaKoordinat]')
87
+ .description('Saatlik hava tahmini (varsayılan 2 gün)')
88
+ .option('-g, --gun <gun>', 'Tahmin gün sayısı (1-7)', '2')
89
+ .action(async (sehirVeyaKoordinat, options) => {
90
+ await havaSaatlik(sehirVeyaKoordinat, options.gun);
91
+ });
92
+
93
+ havaCmd
94
+ .command('kalite [sehirVeyaKoordinat]')
95
+ .description('Güncel hava kalitesi (PM10, PM2.5, CO, NO2)')
96
+ .action(async (sehirVeyaKoordinat) => {
97
+ await havaKalitesi(sehirVeyaKoordinat);
98
+ });
99
+
73
100
  program
74
101
  .command('temizle')
75
102
  .description('Cache ve yapılandırmayı temizle')
@@ -0,0 +1,274 @@
1
+ import axios from 'axios';
2
+ import { CACHE_TTL, getCached, setCached } from '../utils/cache.js';
3
+ import { getCity } from '../utils/config.js';
4
+
5
+ const FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
6
+ const AIR_QUALITY_URL = 'https://air-quality-api.open-meteo.com/v1/air-quality';
7
+ const GEOCODING_URL = 'https://geocoding-api.open-meteo.com/v1/search';
8
+
9
+ const REQUEST_TIMEOUT = 15000;
10
+
11
+ function isCoordinateInput(value) {
12
+ return /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/.test(value);
13
+ }
14
+
15
+ function parseCoordinates(value) {
16
+ const [latRaw, lonRaw] = value.split(',');
17
+ const latitude = Number.parseFloat(latRaw.trim());
18
+ const longitude = Number.parseFloat(lonRaw.trim());
19
+
20
+ if (
21
+ Number.isNaN(latitude)
22
+ || Number.isNaN(longitude)
23
+ || latitude < -90
24
+ || latitude > 90
25
+ || longitude < -180
26
+ || longitude > 180
27
+ ) {
28
+ throw new Error('Koordinatlar geçersiz. Örnek: 41.0082,28.9784');
29
+ }
30
+
31
+ return { latitude, longitude };
32
+ }
33
+
34
+ async function fetchGeocodeByName(cityName) {
35
+ const cacheKey = `geo_${cityName.toLowerCase()}`;
36
+ const cached = getCached(cacheKey);
37
+ if (cached) return cached;
38
+
39
+ const response = await axios.get(GEOCODING_URL, {
40
+ params: {
41
+ name: cityName,
42
+ count: 1,
43
+ language: 'tr',
44
+ format: 'json',
45
+ },
46
+ timeout: REQUEST_TIMEOUT,
47
+ });
48
+
49
+ if (response.status !== 200 || !response.data) {
50
+ throw new Error('Open-Meteo geocoding yanıtı alınamadı.');
51
+ }
52
+
53
+ const first = response.data.results?.[0];
54
+ if (!first) {
55
+ throw new Error(`"${cityName}" için konum bulunamadı.`);
56
+ }
57
+
58
+ const normalized = {
59
+ name: [first.name, first.admin1, first.country].filter(Boolean).join(', '),
60
+ latitude: first.latitude,
61
+ longitude: first.longitude,
62
+ timezone: first.timezone || 'auto',
63
+ };
64
+
65
+ setCached(cacheKey, normalized, CACHE_TTL.DEFAULT);
66
+ return normalized;
67
+ }
68
+
69
+ export async function resolveLocation(inputCity) {
70
+ const raw = (inputCity || getCity() || '').trim();
71
+ if (!raw) {
72
+ throw new Error('Şehir belirtilmedi. Örnek: turkiyem hava guncel istanbul');
73
+ }
74
+
75
+ if (isCoordinateInput(raw)) {
76
+ const coords = parseCoordinates(raw);
77
+ return {
78
+ name: `${coords.latitude},${coords.longitude}`,
79
+ latitude: coords.latitude,
80
+ longitude: coords.longitude,
81
+ timezone: 'auto',
82
+ };
83
+ }
84
+
85
+ return fetchGeocodeByName(raw);
86
+ }
87
+
88
+ function buildHourlyRows(hourly, limitCount) {
89
+ const times = hourly.time || [];
90
+ const temperatures = hourly.temperature_2m || [];
91
+ const apparent = hourly.apparent_temperature || [];
92
+ const precipitation = hourly.precipitation_probability || [];
93
+
94
+ const size = Math.min(times.length, temperatures.length, apparent.length, precipitation.length, limitCount);
95
+ const rows = [];
96
+
97
+ for (let i = 0; i < size; i += 1) {
98
+ rows.push({
99
+ time: times[i],
100
+ temperature: temperatures[i],
101
+ apparentTemperature: apparent[i],
102
+ precipitationProbability: precipitation[i],
103
+ });
104
+ }
105
+
106
+ return rows;
107
+ }
108
+
109
+ function findClosestIndex(timeValues) {
110
+ if (!Array.isArray(timeValues) || timeValues.length === 0) return -1;
111
+
112
+ const nowTs = Date.now();
113
+ let bestIndex = 0;
114
+ let bestDelta = Number.POSITIVE_INFINITY;
115
+
116
+ for (let i = 0; i < timeValues.length; i += 1) {
117
+ const ts = new Date(timeValues[i]).getTime();
118
+ if (Number.isNaN(ts)) continue;
119
+ const delta = Math.abs(nowTs - ts);
120
+ if (delta < bestDelta) {
121
+ bestDelta = delta;
122
+ bestIndex = i;
123
+ }
124
+ }
125
+
126
+ return bestIndex;
127
+ }
128
+
129
+ function normalizeAxiosError(err, sourceName) {
130
+ if (err?.code === 'ECONNABORTED') {
131
+ throw new Error(`${sourceName} isteği zaman aşımına uğradı.`);
132
+ }
133
+ if (err?.code === 'ENOTFOUND' || err?.code === 'ECONNREFUSED') {
134
+ throw new Error(`${sourceName} sunucusuna bağlanılamadı.`);
135
+ }
136
+ if (err?.response?.status) {
137
+ throw new Error(`${sourceName} HTTP ${err.response.status} hatası döndürdü.`);
138
+ }
139
+ throw err;
140
+ }
141
+
142
+ export async function fetchCurrentWeather(inputCity) {
143
+ const location = await resolveLocation(inputCity);
144
+ const cacheKey = `weather_current_${location.latitude}_${location.longitude}`;
145
+ const cached = getCached(cacheKey);
146
+ if (cached) return cached;
147
+
148
+ try {
149
+ const response = await axios.get(FORECAST_URL, {
150
+ params: {
151
+ latitude: location.latitude,
152
+ longitude: location.longitude,
153
+ current: 'temperature_2m,wind_speed_10m,relative_humidity_2m',
154
+ timezone: location.timezone || 'auto',
155
+ },
156
+ timeout: REQUEST_TIMEOUT,
157
+ });
158
+
159
+ if (response.status !== 200 || !response.data?.current) {
160
+ throw new Error('Güncel hava verisi alınamadı.');
161
+ }
162
+
163
+ const result = {
164
+ locationName: location.name,
165
+ latitude: location.latitude,
166
+ longitude: location.longitude,
167
+ timezone: response.data.timezone || location.timezone || 'auto',
168
+ current: {
169
+ time: response.data.current.time,
170
+ temperature: response.data.current.temperature_2m,
171
+ windSpeed: response.data.current.wind_speed_10m,
172
+ humidity: response.data.current.relative_humidity_2m,
173
+ },
174
+ };
175
+
176
+ setCached(cacheKey, result, CACHE_TTL.WEATHER_CURRENT);
177
+ return result;
178
+ } catch (err) {
179
+ normalizeAxiosError(err, 'Open-Meteo');
180
+ }
181
+ }
182
+
183
+ export async function fetchHourlyForecast(inputCity, forecastDays = 2) {
184
+ const location = await resolveLocation(inputCity);
185
+ const safeDays = Number.isFinite(forecastDays)
186
+ ? Math.min(Math.max(Number.parseInt(forecastDays, 10), 1), 7)
187
+ : 2;
188
+
189
+ const cacheKey = `weather_hourly_${location.latitude}_${location.longitude}_${safeDays}`;
190
+ const cached = getCached(cacheKey);
191
+ if (cached) return cached;
192
+
193
+ try {
194
+ const response = await axios.get(FORECAST_URL, {
195
+ params: {
196
+ latitude: location.latitude,
197
+ longitude: location.longitude,
198
+ hourly: 'temperature_2m,apparent_temperature,precipitation_probability',
199
+ forecast_days: safeDays,
200
+ timezone: location.timezone || 'auto',
201
+ },
202
+ timeout: REQUEST_TIMEOUT,
203
+ });
204
+
205
+ if (response.status !== 200 || !response.data?.hourly) {
206
+ throw new Error('Saatlik tahmin verisi alınamadı.');
207
+ }
208
+
209
+ const limitCount = Math.min(safeDays * 24, 48);
210
+ const rows = buildHourlyRows(response.data.hourly, limitCount);
211
+
212
+ const result = {
213
+ locationName: location.name,
214
+ latitude: location.latitude,
215
+ longitude: location.longitude,
216
+ timezone: response.data.timezone || location.timezone || 'auto',
217
+ rows,
218
+ forecastDays: safeDays,
219
+ };
220
+
221
+ setCached(cacheKey, result, CACHE_TTL.WEATHER_HOURLY);
222
+ return result;
223
+ } catch (err) {
224
+ normalizeAxiosError(err, 'Open-Meteo');
225
+ }
226
+ }
227
+
228
+ export async function fetchAirQuality(inputCity) {
229
+ const location = await resolveLocation(inputCity);
230
+ const cacheKey = `weather_air_${location.latitude}_${location.longitude}`;
231
+ const cached = getCached(cacheKey);
232
+ if (cached) return cached;
233
+
234
+ try {
235
+ const response = await axios.get(AIR_QUALITY_URL, {
236
+ params: {
237
+ latitude: location.latitude,
238
+ longitude: location.longitude,
239
+ hourly: 'pm10,pm2_5,carbon_monoxide,nitrogen_dioxide',
240
+ timezone: 'auto',
241
+ },
242
+ timeout: REQUEST_TIMEOUT,
243
+ });
244
+
245
+ const hourly = response.data?.hourly;
246
+ if (response.status !== 200 || !hourly) {
247
+ throw new Error('Hava kalitesi verisi alınamadı.');
248
+ }
249
+
250
+ const idx = findClosestIndex(hourly.time);
251
+ if (idx < 0) {
252
+ throw new Error('Hava kalitesi saat verisi çözümlenemedi.');
253
+ }
254
+
255
+ const result = {
256
+ locationName: location.name,
257
+ latitude: location.latitude,
258
+ longitude: location.longitude,
259
+ timezone: response.data.timezone || 'auto',
260
+ current: {
261
+ time: hourly.time?.[idx] ?? '-',
262
+ pm10: hourly.pm10?.[idx] ?? null,
263
+ pm25: hourly.pm2_5?.[idx] ?? null,
264
+ carbonMonoxide: hourly.carbon_monoxide?.[idx] ?? null,
265
+ nitrogenDioxide: hourly.nitrogen_dioxide?.[idx] ?? null,
266
+ },
267
+ };
268
+
269
+ setCached(cacheKey, result, CACHE_TTL.WEATHER_AIR);
270
+ return result;
271
+ } catch (err) {
272
+ normalizeAxiosError(err, 'Open-Meteo Air Quality');
273
+ }
274
+ }
@@ -22,6 +22,9 @@ export function printHelp() {
22
22
  console.log(chalk.white.bold(' Komutlar:\n'));
23
23
  console.log(chalk.cyan(' turkiyem sehir <ankara|istanbul>') + chalk.gray(' Şehir seç'));
24
24
  console.log(chalk.cyan(' turkiyem hat <numara>') + chalk.gray(' Hat sorgula'));
25
+ console.log(chalk.cyan(' turkiyem hava guncel [sehir|lat,lon]') + chalk.gray(' Güncel hava'));
26
+ console.log(chalk.cyan(' turkiyem hava saatlik [sehir|lat,lon] -g 2') + chalk.gray(' Saatlik tahmin'));
27
+ console.log(chalk.cyan(' turkiyem hava kalite [sehir|lat,lon]') + chalk.gray(' Hava kalitesi'));
25
28
  console.log(chalk.cyan(' turkiyem deprem son24') + chalk.gray(' Son 24 saat depremler'));
26
29
  console.log(chalk.cyan(' turkiyem deprem 7gun') + chalk.gray(' Son 7 gün depremler'));
27
30
  console.log(chalk.cyan(' turkiyem deprem buyukluk <deger>') + chalk.gray(' Büyüklüğe göre filtrele'));
@@ -3,6 +3,9 @@ import NodeCache from 'node-cache';
3
3
  export const CACHE_TTL = Object.freeze({
4
4
  DEFAULT: 300,
5
5
  IETT_SOAP: 90,
6
+ WEATHER_CURRENT: 300,
7
+ WEATHER_HOURLY: 600,
8
+ WEATHER_AIR: 600,
6
9
  });
7
10
 
8
11
  const cache = new NodeCache({ stdTTL: CACHE_TTL.DEFAULT, checkperiod: 60 });
@@ -135,3 +135,66 @@ export function createIettPlannedTimesTable(plannedTimes) {
135
135
 
136
136
  return table.toString();
137
137
  }
138
+
139
+ export function createCurrentWeatherTable(result) {
140
+ const table = new Table({
141
+ style: { head: [], border: ['gray'] },
142
+ wordWrap: true,
143
+ });
144
+
145
+ table.push(
146
+ { [chalk.cyan('Konum')]: result.locationName || '-' },
147
+ { [chalk.cyan('Koordinat')]: `${result.latitude}, ${result.longitude}` },
148
+ { [chalk.cyan('Zaman Dilimi')]: result.timezone || '-' },
149
+ { [chalk.cyan('Ölçüm Zamanı')]: result.current?.time || '-' },
150
+ { [chalk.cyan('Sıcaklık (°C)')]: String(result.current?.temperature ?? '-') },
151
+ { [chalk.cyan('Rüzgar (km/s)')]: String(result.current?.windSpeed ?? '-') },
152
+ { [chalk.cyan('Nem (%)')]: String(result.current?.humidity ?? '-') },
153
+ );
154
+
155
+ return table.toString();
156
+ }
157
+
158
+ export function createHourlyWeatherTable(result) {
159
+ const table = new Table({
160
+ head: [
161
+ chalk.white.bold('Saat'),
162
+ chalk.white.bold('Sıcaklık (°C)'),
163
+ chalk.white.bold('Hissedilen (°C)'),
164
+ chalk.white.bold('Yağış Olasılığı (%)'),
165
+ ],
166
+ colWidths: [22, 15, 18, 22],
167
+ style: { head: [], border: ['gray'] },
168
+ });
169
+
170
+ for (const row of result.rows || []) {
171
+ table.push([
172
+ row.time || '-',
173
+ String(row.temperature ?? '-'),
174
+ String(row.apparentTemperature ?? '-'),
175
+ String(row.precipitationProbability ?? '-'),
176
+ ]);
177
+ }
178
+
179
+ return table.toString();
180
+ }
181
+
182
+ export function createAirQualityTable(result) {
183
+ const table = new Table({
184
+ style: { head: [], border: ['gray'] },
185
+ wordWrap: true,
186
+ });
187
+
188
+ table.push(
189
+ { [chalk.cyan('Konum')]: result.locationName || '-' },
190
+ { [chalk.cyan('Koordinat')]: `${result.latitude}, ${result.longitude}` },
191
+ { [chalk.cyan('Zaman Dilimi')]: result.timezone || '-' },
192
+ { [chalk.cyan('Ölçüm Zamanı')]: result.current?.time || '-' },
193
+ { [chalk.cyan('PM10 (µg/m3)')]: String(result.current?.pm10 ?? '-') },
194
+ { [chalk.cyan('PM2.5 (µg/m3)')]: String(result.current?.pm25 ?? '-') },
195
+ { [chalk.cyan('CO (µg/m3)')]: String(result.current?.carbonMonoxide ?? '-') },
196
+ { [chalk.cyan('NO2 (µg/m3)')]: String(result.current?.nitrogenDioxide ?? '-') },
197
+ );
198
+
199
+ return table.toString();
200
+ }