@telemetryos/cli 1.8.3 → 1.10.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/commands/auth.js +4 -4
  3. package/dist/commands/init.js +90 -42
  4. package/dist/commands/publish.d.ts +2 -0
  5. package/dist/commands/publish.js +208 -0
  6. package/dist/index.js +2 -0
  7. package/dist/plugins/math-tools.d.ts +2 -0
  8. package/dist/plugins/math-tools.js +18 -0
  9. package/dist/services/api-client.d.ts +18 -0
  10. package/dist/services/api-client.js +70 -0
  11. package/dist/services/archiver.d.ts +4 -0
  12. package/dist/services/archiver.js +65 -0
  13. package/dist/services/build-poller.d.ts +10 -0
  14. package/dist/services/build-poller.js +63 -0
  15. package/dist/services/cli-config.d.ts +6 -0
  16. package/dist/services/cli-config.js +23 -0
  17. package/dist/services/generate-application.d.ts +2 -1
  18. package/dist/services/generate-application.js +31 -32
  19. package/dist/services/project-config.d.ts +24 -0
  20. package/dist/services/project-config.js +51 -0
  21. package/dist/services/run-server.js +29 -73
  22. package/dist/types/api.d.ts +44 -0
  23. package/dist/types/api.js +1 -0
  24. package/dist/types/applications.d.ts +44 -0
  25. package/dist/types/applications.js +1 -0
  26. package/dist/utils/ansi.d.ts +10 -0
  27. package/dist/utils/ansi.js +10 -0
  28. package/dist/utils/path-utils.d.ts +55 -0
  29. package/dist/utils/path-utils.js +99 -0
  30. package/package.json +4 -2
  31. package/templates/vite-react-typescript/CLAUDE.md +6 -5
  32. package/templates/vite-react-typescript/_claude/settings.local.json +2 -1
  33. package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
  34. package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +367 -130
  35. package/templates/vite-react-typescript/_claude/skills/tos-weather-api/SKILL.md +443 -269
@@ -1,162 +1,61 @@
1
1
  ---
2
2
  name: tos-weather-api
3
- description: Integrate TelemetryOS Weather API for current conditions and forecasts. Use when building weather displays or any app that needs location-based weather data.
3
+ description: Integrate TelemetryOS Weather API for current conditions, forecasts, and alerts. Use when building weather displays or apps needing location-based weather data.
4
4
  ---
5
5
 
6
6
  # TelemetryOS Weather API
7
7
 
8
- The Weather API provides current conditions, hourly forecasts, and daily forecasts for any location.
8
+ Access weather data including current conditions, daily forecasts, hourly forecasts, and severe weather alerts. All responses include both metric and imperial units.
9
9
 
10
- ## Quick Reference
11
-
12
- ```typescript
13
- import { weather } from '@telemetryos/sdk'
14
-
15
- // Current conditions
16
- const conditions = await weather().getConditions({
17
- city: 'New York',
18
- units: 'imperial'
19
- })
20
-
21
- // Hourly forecast (next 24 hours)
22
- const hourly = await weather().getHourlyForecast({
23
- city: 'New York',
24
- units: 'imperial',
25
- hours: 24
26
- })
27
-
28
- // Daily forecast (next 7 days)
29
- const daily = await weather().getDailyForecast({
30
- city: 'New York',
31
- units: 'imperial',
32
- days: 7
33
- })
34
- ```
35
-
36
- ## Location Parameters
37
-
38
- Specify location using ONE of these methods:
39
-
40
- ### By City Name
41
-
42
- ```typescript
43
- // City only
44
- { city: 'New York' }
45
-
46
- // City with country
47
- { city: 'London, UK' }
10
+ ## Key Concepts
48
11
 
49
- // City with state (US)
50
- { city: 'Portland, OR' }
51
- ```
12
+ - **City-based lookup**: Use `getCities()` to search for locations and obtain a `cityId`
13
+ - **Dual-unit responses**: Every response includes both metric (Celsius, km/h) and imperial (Fahrenheit, mph)
14
+ - **No units parameter**: You don't specify units - both are always returned
52
15
 
53
- ### By Postal Code
54
-
55
- ```typescript
56
- { postalCode: '10001' }
57
- ```
58
-
59
- ### By Coordinates
16
+ ## Quick Reference
60
17
 
61
18
  ```typescript
62
- { lat: '40.7128', lon: '-74.0060' }
63
- ```
64
-
65
- ## Units
19
+ import { weather } from '@telemetryos/sdk'
66
20
 
67
- ```typescript
68
- // Fahrenheit, miles, etc.
69
- { units: 'imperial' }
21
+ // 1. Search for cities (use in Settings)
22
+ const cities = await weather.getCities({ search: 'Seattle' })
23
+ const cityId = cities[0].cityId
70
24
 
71
- // Celsius, kilometers, etc.
72
- { units: 'metric' }
73
- ```
25
+ // 2. Get current conditions
26
+ const conditions = await weather.getConditions({ cityId })
74
27
 
75
- ## Response Types
28
+ // 3. Get daily forecast (up to 16 days)
29
+ const daily = await weather.getDailyForecast({ cityId, days: 7 })
76
30
 
77
- ### WeatherConditions (Current)
31
+ // 4. Get hourly forecast (up to 120 hours)
32
+ const hourly = await weather.getHourlyForecast({ cityId, hours: 24 })
78
33
 
79
- ```typescript
80
- interface WeatherConditions {
81
- EpochTime: number
82
- WeatherText: string
83
- WeatherIcon: number
84
- HasPrecipitation: boolean
85
- PrecipitationType: string | null
86
- IsDayTime: boolean
87
- Temperature: {
88
- Metric: { Value: number; Unit: string }
89
- Imperial: { Value: number; Unit: string }
90
- }
91
- RealFeelTemperature: {
92
- Metric: { Value: number; Unit: string }
93
- Imperial: { Value: number; Unit: string }
94
- }
95
- RelativeHumidity: number
96
- Wind: {
97
- Direction: { Degrees: number; English: string }
98
- Speed: {
99
- Metric: { Value: number; Unit: string }
100
- Imperial: { Value: number; Unit: string }
101
- }
102
- }
103
- UVIndex: number
104
- UVIndexText: string
105
- Visibility: {
106
- Metric: { Value: number; Unit: string }
107
- Imperial: { Value: number; Unit: string }
108
- }
109
- CloudCover: number
110
- Pressure: {
111
- Metric: { Value: number; Unit: string }
112
- Imperial: { Value: number; Unit: string }
113
- }
114
- }
34
+ // 5. Get weather alerts
35
+ const alerts = await weather.getAlerts({ cityId })
115
36
  ```
116
37
 
117
- ### WeatherForecast (Hourly/Daily)
118
-
119
- ```typescript
120
- interface WeatherForecast {
121
- DateTime: string
122
- EpochDateTime: number
123
- WeatherIcon: number
124
- IconPhrase: string
125
- HasPrecipitation: boolean
126
- PrecipitationType?: string
127
- PrecipitationIntensity?: string
128
- IsDaylight: boolean
129
- Temperature: {
130
- Value: number
131
- Unit: string
132
- }
133
- RealFeelTemperature: {
134
- Value: number
135
- Unit: string
136
- }
137
- Wind: {
138
- Speed: { Value: number; Unit: string }
139
- Direction: { Degrees: number; English: string }
140
- }
141
- RelativeHumidity: number
142
- PrecipitationProbability: number
143
- }
144
- ```
38
+ ## City Selection Pattern
145
39
 
146
- ## Complete Example
40
+ Cities are selected in Settings, weather is displayed in Render. This is the standard workflow.
147
41
 
148
- ### Settings (City + Units Selection)
42
+ ### Store Hook
149
43
 
150
44
  ```typescript
151
45
  // hooks/store.ts
152
46
  import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
153
47
 
154
- export const useCityState = createUseInstanceStoreState<string>('city', '')
155
- export const useUnitsState = createUseInstanceStoreState<'imperial' | 'metric'>('units', 'imperial')
48
+ // Store the cityId (number) - NOT the city name
49
+ export const useCityIdState = createUseInstanceStoreState<number | null>('cityId', null)
50
+ export const useCityNameState = createUseInstanceStoreState<string>('cityName', '')
156
51
  ```
157
52
 
53
+ ### Settings - City Search & Select
54
+
158
55
  ```typescript
159
56
  // views/Settings.tsx
57
+ import { useState, useEffect } from 'react'
58
+ import { weather, WeatherCity } from '@telemetryos/sdk'
160
59
  import {
161
60
  SettingsContainer,
162
61
  SettingsField,
@@ -164,86 +63,113 @@ import {
164
63
  SettingsInputFrame,
165
64
  SettingsSelectFrame,
166
65
  } from '@telemetryos/sdk/react'
167
- import { useCityState, useUnitsState } from '../hooks/store'
66
+ import { useCityIdState, useCityNameState } from '../hooks/store'
168
67
 
169
68
  export default function Settings() {
170
- const [isLoadingCity, city, setCity] = useCityState()
171
- const [isLoadingUnits, units, setUnits] = useUnitsState()
69
+ const [isLoadingId, cityId, setCityId] = useCityIdState()
70
+ const [isLoadingName, cityName, setCityName] = useCityNameState()
71
+
72
+ const [searchQuery, setSearchQuery] = useState('')
73
+ const [searchResults, setSearchResults] = useState<WeatherCity[]>([])
74
+ const [isSearching, setIsSearching] = useState(false)
75
+
76
+ // Search for cities when query changes
77
+ useEffect(() => {
78
+ if (searchQuery.length < 2) {
79
+ setSearchResults([])
80
+ return
81
+ }
82
+
83
+ const searchCities = async () => {
84
+ setIsSearching(true)
85
+ try {
86
+ const cities = await weather.getCities({ search: searchQuery })
87
+ setSearchResults(cities.slice(0, 10)) // Limit results
88
+ } catch (err) {
89
+ console.error('City search failed:', err)
90
+ } finally {
91
+ setIsSearching(false)
92
+ }
93
+ }
94
+
95
+ // Debounce search
96
+ const timer = setTimeout(searchCities, 300)
97
+ return () => clearTimeout(timer)
98
+ }, [searchQuery])
99
+
100
+ const handleCitySelect = (city: WeatherCity) => {
101
+ setCityId(city.cityId)
102
+ // Store display name for UI
103
+ const displayName = city.stateName
104
+ ? `${city.cityName}, ${city.stateCode || city.stateName}`
105
+ : `${city.cityName}, ${city.countryCode}`
106
+ setCityName(displayName)
107
+ setSearchQuery('')
108
+ setSearchResults([])
109
+ }
172
110
 
173
111
  return (
174
112
  <SettingsContainer>
175
113
  <SettingsField>
176
114
  <SettingsLabel>City</SettingsLabel>
115
+ {cityName && <p style={{ margin: '4px 0 8px' }}>Selected: {cityName}</p>}
177
116
  <SettingsInputFrame>
178
117
  <input
179
118
  type="text"
180
- placeholder="Enter city name..."
181
- disabled={isLoadingCity}
182
- value={city}
183
- onChange={(e) => setCity(e.target.value)}
119
+ placeholder="Search for a city..."
120
+ value={searchQuery}
121
+ onChange={(e) => setSearchQuery(e.target.value)}
184
122
  />
185
123
  </SettingsInputFrame>
186
- </SettingsField>
187
-
188
- <SettingsField>
189
- <SettingsLabel>Temperature Units</SettingsLabel>
190
- <SettingsSelectFrame>
191
- <select
192
- disabled={isLoadingUnits}
193
- value={units}
194
- onChange={(e) => setUnits(e.target.value as 'imperial' | 'metric')}
195
- >
196
- <option value="imperial">Fahrenheit</option>
197
- <option value="metric">Celsius</option>
198
- </select>
199
- </SettingsSelectFrame>
124
+ {searchResults.length > 0 && (
125
+ <SettingsSelectFrame>
126
+ <select
127
+ size={Math.min(searchResults.length, 5)}
128
+ onChange={(e) => {
129
+ const city = searchResults.find(c => c.cityId === Number(e.target.value))
130
+ if (city) handleCitySelect(city)
131
+ }}
132
+ >
133
+ {searchResults.map(city => (
134
+ <option key={city.cityId} value={city.cityId}>
135
+ {city.cityName}, {city.stateCode || city.stateName || city.countryCode}
136
+ </option>
137
+ ))}
138
+ </select>
139
+ </SettingsSelectFrame>
140
+ )}
200
141
  </SettingsField>
201
142
  </SettingsContainer>
202
143
  )
203
144
  }
204
145
  ```
205
146
 
206
- ### Render (Weather Display)
147
+ ### Render - Weather Display
207
148
 
208
149
  ```typescript
209
150
  // views/Render.tsx
210
151
  import { useEffect, useState } from 'react'
211
- import { weather } from '@telemetryos/sdk'
212
- import { useCityState, useUnitsState } from '../hooks/store'
213
-
214
- interface WeatherData {
215
- temperature: number
216
- description: string
217
- humidity: number
218
- windSpeed: number
219
- }
152
+ import { weather, WeatherConditions } from '@telemetryos/sdk'
153
+ import { useCityIdState, useCityNameState } from '../hooks/store'
220
154
 
221
155
  export default function Render() {
222
- const [isLoadingCity, city] = useCityState()
223
- const [isLoadingUnits, units] = useUnitsState()
156
+ const [isLoadingId, cityId] = useCityIdState()
157
+ const [isLoadingName, cityName] = useCityNameState()
224
158
 
225
- const [data, setData] = useState<WeatherData | null>(null)
159
+ const [conditions, setConditions] = useState<WeatherConditions | null>(null)
226
160
  const [loading, setLoading] = useState(false)
227
161
  const [error, setError] = useState<string | null>(null)
228
162
 
229
163
  useEffect(() => {
230
- if (isLoadingCity || isLoadingUnits || !city) return
164
+ if (isLoadingId || !cityId) return
231
165
 
232
166
  const fetchWeather = async () => {
233
167
  setLoading(true)
234
168
  setError(null)
235
169
 
236
170
  try {
237
- const conditions = await weather().getConditions({ city, units })
238
-
239
- const tempKey = units === 'imperial' ? 'Imperial' : 'Metric'
240
-
241
- setData({
242
- temperature: conditions.Temperature[tempKey].Value,
243
- description: conditions.WeatherText,
244
- humidity: conditions.RelativeHumidity,
245
- windSpeed: conditions.Wind.Speed[tempKey].Value,
246
- })
171
+ const data = await weather.getConditions({ cityId })
172
+ setConditions(data)
247
173
  } catch (err) {
248
174
  setError(err instanceof Error ? err.message : 'Failed to fetch weather')
249
175
  } finally {
@@ -253,132 +179,380 @@ export default function Render() {
253
179
 
254
180
  fetchWeather()
255
181
 
256
- // Refresh every 10 minutes
257
- const interval = setInterval(fetchWeather, 10 * 60 * 1000)
182
+ // Refresh every 15 minutes
183
+ const interval = setInterval(fetchWeather, 15 * 60 * 1000)
258
184
  return () => clearInterval(interval)
259
- }, [city, units, isLoadingCity, isLoadingUnits])
185
+ }, [cityId, isLoadingId])
260
186
 
261
187
  // Loading states
262
- if (isLoadingCity || isLoadingUnits) return <div>Loading config...</div>
263
- if (!city) return <div>Configure city in Settings</div>
264
- if (loading && !data) return <div>Loading weather...</div>
265
- if (error && !data) return <div>Error: {error}</div>
266
-
267
- const unitSymbol = units === 'imperial' ? '°F' : '°C'
268
- const speedUnit = units === 'imperial' ? 'mph' : 'km/h'
188
+ if (isLoadingId || isLoadingName) return <div>Loading config...</div>
189
+ if (!cityId) return <div>Configure city in Settings</div>
190
+ if (loading && !conditions) return <div>Loading weather...</div>
191
+ if (error && !conditions) return <div>Error: {error}</div>
192
+ if (!conditions) return null
269
193
 
270
194
  return (
271
195
  <div className="weather-display">
272
- <h1>{city}</h1>
273
- {data && (
274
- <>
275
- <div className="temperature">
276
- {Math.round(data.temperature)}{unitSymbol}
277
- </div>
278
- <div className="description">{data.description}</div>
279
- <div className="details">
280
- <span>Humidity: {data.humidity}%</span>
281
- <span>Wind: {data.windSpeed} {speedUnit}</span>
282
- </div>
283
- </>
284
- )}
196
+ <h1>{cityName || conditions.cityName}</h1>
197
+ <div className="temperature">
198
+ {Math.round(conditions.temperatureC)}°C / {Math.round(conditions.temperatureF)}°F
199
+ </div>
200
+ <div className="description">{conditions.weatherDescription}</div>
201
+ <div className="details">
202
+ <span>Feels like: {Math.round(conditions.temperatureFeelsLikeC)}°C</span>
203
+ <span>Humidity: {conditions.humidity}%</span>
204
+ <span>Wind: {conditions.windSpeedKph} km/h {conditions.windDirectionShort}</span>
205
+ </div>
285
206
  </div>
286
207
  )
287
208
  }
288
209
  ```
289
210
 
290
- ## Forecast Example
211
+ ## Methods
212
+
213
+ ### getCities()
214
+
215
+ Search for cities by name or country code to obtain a `cityId`.
291
216
 
292
217
  ```typescript
293
- import { weather } from '@telemetryos/sdk'
218
+ type CitiesSearchParams = {
219
+ countryCode?: string // ISO country code (e.g., "US", "CA")
220
+ search?: string // City name (case-insensitive prefix match)
221
+ }
222
+
223
+ const cities = await weather.getCities({ search: 'Vancouver' })
224
+ const cities = await weather.getCities({ search: 'Portland', countryCode: 'US' })
225
+ ```
226
+
227
+ ### getConditions()
228
+
229
+ Get current weather conditions for a city.
230
+
231
+ ```typescript
232
+ type WeatherConditionsParams = {
233
+ cityId: number // Required - from getCities()
234
+ language?: string // Language code for localized descriptions
235
+ }
236
+
237
+ const conditions = await weather.getConditions({ cityId: 5128581 })
238
+ ```
239
+
240
+ ### getDailyForecast()
241
+
242
+ Get daily forecast for up to 16 days.
243
+
244
+ ```typescript
245
+ type DailyForecastParams = {
246
+ cityId: number
247
+ language?: string
248
+ days?: number // Default: 5, Max: 16
249
+ }
250
+
251
+ const forecast = await weather.getDailyForecast({ cityId: 5128581, days: 7 })
294
252
 
295
- // Get 5-day forecast
296
- const forecast = await weather().getDailyForecast({
297
- city: 'New York',
298
- units: 'imperial',
299
- days: 5
253
+ forecast.data.forEach(day => {
254
+ console.log(`${day.forecastDate}: ${day.weatherDescription}`)
255
+ console.log(` High: ${day.maxTemperatureC}°C / ${day.maxTemperatureF}°F`)
256
+ console.log(` Low: ${day.minTemperatureC}°C / ${day.minTemperatureF}°F`)
257
+ console.log(` Precip: ${day.precipitationProbability}%`)
300
258
  })
259
+ ```
260
+
261
+ ### getHourlyForecast()
262
+
263
+ Get hourly forecast for up to 120 hours.
264
+
265
+ ```typescript
266
+ type HourlyForecastParams = {
267
+ cityId: number
268
+ language?: string
269
+ hours?: number // Default: 12, Max: 120
270
+ }
271
+
272
+ const forecast = await weather.getHourlyForecast({ cityId: 5128581, hours: 24 })
301
273
 
302
- forecast.forEach(day => {
303
- console.log(`${day.DateTime}: ${day.Temperature.Value - ${day.IconPhrase}`)
274
+ forecast.data.forEach(hour => {
275
+ console.log(`${hour.forecastTimeLocal}: ${hour.temperatureCC`)
276
+ console.log(` ${hour.weatherDescription}`)
304
277
  })
305
278
  ```
306
279
 
307
- ## Weather Icons
280
+ ### getAlerts()
281
+
282
+ Get severe weather alerts and warnings.
283
+
284
+ ```typescript
285
+ type WeatherAlertsParams = {
286
+ cityId: number
287
+ language?: string
288
+ }
289
+
290
+ const result = await weather.getAlerts({ cityId: 5128581 })
291
+
292
+ if (result.alerts.length > 0) {
293
+ result.alerts.forEach(alert => {
294
+ console.log(`[${alert.severity}] ${alert.title}`)
295
+ console.log(` Effective: ${alert.effectiveLocal}`)
296
+ console.log(` Expires: ${alert.expiresLocal}`)
297
+ })
298
+ }
299
+ ```
300
+
301
+ ## Response Types
302
+
303
+ ### WeatherCity
304
+
305
+ ```typescript
306
+ type WeatherCity = {
307
+ cityId: number // Use this for weather queries
308
+ cityName: string
309
+ stateName?: string // Full state/province name
310
+ stateCode?: string // US only: "WA", "CA", etc.
311
+ countryName: string
312
+ countryCode: string // ISO code: "US", "CA", etc.
313
+ latitude: number
314
+ longitude: number
315
+ }
316
+ ```
317
+
318
+ ### WeatherConditions
319
+
320
+ ```typescript
321
+ type WeatherConditions = {
322
+ // Location
323
+ latitude: number
324
+ longitude: number
325
+ cityName: string
326
+ stateName?: string
327
+ stateCode?: string // US only
328
+ countryName: string
329
+ countryCode: string
330
+ timezone: string // IANA timezone
331
+
332
+ // Time
333
+ observedAtSec: number // Unix timestamp
334
+ observedAtLocal: string // "2025-01-15T14:30:00-08:00"
335
+ sunrise: string // "07:23"
336
+ sunset: string // "17:45"
337
+ partOfDay: 'day' | 'night'
338
+
339
+ // Weather
340
+ weatherCode: number
341
+ weatherDescription: string
342
+ weatherIcon: string
343
+
344
+ // Temperature (dual-unit)
345
+ temperatureC: number
346
+ temperatureF: number
347
+ temperatureFeelsLikeC: number
348
+ temperatureFeelsLikeF: number
349
+ dewPointC: number
350
+ dewPointF: number
351
+
352
+ // Wind (dual-unit)
353
+ windSpeedKph: number
354
+ windSpeedMph: number
355
+ gustSpeedKph: number
356
+ gustSpeedMph: number
357
+ windDirectionDeg: number
358
+ windDirectionFull: string // "North-Northwest"
359
+ windDirectionShort: string // "NNW"
360
+
361
+ // Atmospheric (dual-unit)
362
+ pressureMb: number
363
+ pressureInHg: number
364
+ visibilityKm: number
365
+ visibilityMi: number
366
+ humidity: number // Percentage
367
+ clouds: number // Percentage
368
+
369
+ // Precipitation (dual-unit)
370
+ precipitationRateMmph: number
371
+ precipitationRateInph: number
372
+ snowfallRateMmph: number
373
+ snowfallRateInph: number
374
+
375
+ // Indices
376
+ uvIndex: number
377
+ airQualityIndex: number
378
+ }
379
+ ```
380
+
381
+ ### DailyForecastData
382
+
383
+ ```typescript
384
+ type DailyForecastData = {
385
+ forecastDate: string // "2025-01-15"
386
+ forecastDateSec: number // Unix timestamp
387
+
388
+ // Weather
389
+ weatherCode: number
390
+ weatherDescription: string
391
+ weatherIcon: string
392
+ precipitationProbability: number // 0-100
393
+
394
+ // Temperature (dual-unit)
395
+ temperatureC: number // Average
396
+ temperatureF: number
397
+ maxTemperatureC: number
398
+ maxTemperatureF: number
399
+ minTemperatureC: number
400
+ minTemperatureF: number
401
+ highTemperatureC: number // Daytime high
402
+ highTemperatureF: number
403
+ lowTemperatureC: number // Nighttime low
404
+ lowTemperatureF: number
405
+
406
+ // Wind, Atmospheric, Precipitation - same pattern as conditions
407
+
408
+ // Moon/Sun
409
+ sunrise: string
410
+ sunset: string
411
+ moonrise: string
412
+ moonset: string
413
+ moonIllumination: number // 0-1 fraction
414
+ moonLunationPhase: number // 0=New, 0.5=Full
415
+ }
416
+ ```
417
+
418
+ ### HourlyForecastData
419
+
420
+ ```typescript
421
+ type HourlyForecastData = {
422
+ forecastTimeSec: number // Unix timestamp
423
+ forecastTimeLocal: string // "2025-01-15T15:00:00-08:00"
424
+ forecastTimeUtc: string // "2025-01-15T23:00:00Z"
425
+ partOfDay: 'day' | 'night'
426
+
427
+ // Weather
428
+ weatherCode: number
429
+ weatherDescription: string
430
+ weatherIcon: string
431
+ precipitationProbability: number
432
+
433
+ // Temperature, Wind, Atmospheric, Precipitation - same pattern as conditions
434
+ }
435
+ ```
308
436
 
309
- The API returns `WeatherIcon` as a number (1-44). Map to your icon set:
437
+ ### WeatherAlert
310
438
 
311
439
  ```typescript
312
- const iconMap: Record<number, string> = {
313
- 1: 'sunny',
314
- 2: 'mostly-sunny',
315
- 3: 'partly-sunny',
316
- 4: 'intermittent-clouds',
317
- 5: 'hazy-sunshine',
318
- 6: 'mostly-cloudy',
319
- 7: 'cloudy',
320
- 8: 'dreary',
321
- 11: 'fog',
322
- 12: 'showers',
323
- 13: 'mostly-cloudy-showers',
324
- 14: 'partly-sunny-showers',
325
- 15: 'thunderstorms',
326
- 16: 'mostly-cloudy-thunderstorms',
327
- 17: 'partly-sunny-thunderstorms',
328
- 18: 'rain',
329
- 19: 'flurries',
330
- 20: 'mostly-cloudy-flurries',
331
- 21: 'partly-sunny-flurries',
332
- 22: 'snow',
333
- 23: 'mostly-cloudy-snow',
334
- 24: 'ice',
335
- 25: 'sleet',
336
- 26: 'freezing-rain',
337
- 29: 'rain-and-snow',
338
- 30: 'hot',
339
- 31: 'cold',
340
- 32: 'windy',
341
- 33: 'clear-night',
342
- 34: 'mostly-clear-night',
343
- 35: 'partly-cloudy-night',
344
- 36: 'intermittent-clouds-night',
345
- 37: 'hazy-moonlight',
346
- 38: 'mostly-cloudy-night',
347
- 39: 'partly-cloudy-showers-night',
348
- 40: 'mostly-cloudy-showers-night',
349
- 41: 'partly-cloudy-thunderstorms-night',
350
- 42: 'mostly-cloudy-thunderstorms-night',
351
- 43: 'mostly-cloudy-flurries-night',
352
- 44: 'mostly-cloudy-snow-night',
440
+ type WeatherAlertSeverity = 'Advisory' | 'Watch' | 'Warning'
441
+
442
+ type WeatherAlert = {
443
+ title: string
444
+ description: string
445
+ severity: WeatherAlertSeverity
446
+
447
+ // Times
448
+ effectiveSec: number
449
+ effectiveUtc: string
450
+ effectiveLocal: string
451
+ expiresSec: number
452
+ expiresUtc: string
453
+ expiresLocal: string
454
+ onsetSec?: number // When event starts (optional)
455
+ onsetLocal?: string
456
+ endsSec?: number // When event ends (optional)
457
+ endsLocal?: string
458
+
459
+ uri: string // Link for more info
460
+ regions: string[] // Affected regions
353
461
  }
462
+ ```
463
+
464
+ ## Forecast Display Example
465
+
466
+ ```typescript
467
+ // 5-Day Forecast Component
468
+ import { useEffect, useState } from 'react'
469
+ import { weather, DailyForecastData } from '@telemetryos/sdk'
470
+ import { useCityIdState } from '../hooks/store'
354
471
 
355
- function getIconName(iconNumber: number): string {
356
- return iconMap[iconNumber] || 'unknown'
472
+ export default function FiveDayForecast() {
473
+ const [, cityId] = useCityIdState()
474
+ const [forecast, setForecast] = useState<DailyForecastData[]>([])
475
+
476
+ useEffect(() => {
477
+ if (!cityId) return
478
+ weather.getDailyForecast({ cityId, days: 5 })
479
+ .then(result => setForecast(result.data))
480
+ }, [cityId])
481
+
482
+ return (
483
+ <div className="forecast-row">
484
+ {forecast.map(day => (
485
+ <div key={day.forecastDate} className="forecast-day">
486
+ <div className="date">
487
+ {new Date(day.forecastDate).toLocaleDateString('en-US', { weekday: 'short' })}
488
+ </div>
489
+ <div className="high">{Math.round(day.maxTemperatureC)}°</div>
490
+ <div className="low">{Math.round(day.minTemperatureC)}°</div>
491
+ <div className="description">{day.weatherDescription}</div>
492
+ </div>
493
+ ))}
494
+ </div>
495
+ )
357
496
  }
358
497
  ```
359
498
 
360
- ## Error Handling
499
+ ## Weather Alerts Example
361
500
 
362
501
  ```typescript
363
- try {
364
- const conditions = await weather().getConditions({ city, units })
365
- } catch (err) {
366
- if (err instanceof Error) {
367
- if (err.message.includes('timeout')) {
368
- // Request timed out (30 second limit)
369
- } else if (err.message.includes('not found')) {
370
- // City not found
371
- } else {
372
- // Other error
502
+ // Alert Banner Component
503
+ import { useEffect, useState } from 'react'
504
+ import { weather, WeatherAlert } from '@telemetryos/sdk'
505
+ import { useCityIdState } from '../hooks/store'
506
+
507
+ export default function AlertBanner() {
508
+ const [, cityId] = useCityIdState()
509
+ const [alerts, setAlerts] = useState<WeatherAlert[]>([])
510
+
511
+ useEffect(() => {
512
+ if (!cityId) return
513
+
514
+ const fetchAlerts = async () => {
515
+ const result = await weather.getAlerts({ cityId })
516
+ setAlerts(result.alerts)
373
517
  }
374
- }
518
+
519
+ fetchAlerts()
520
+ const interval = setInterval(fetchAlerts, 15 * 60 * 1000)
521
+ return () => clearInterval(interval)
522
+ }, [cityId])
523
+
524
+ if (alerts.length === 0) return null
525
+
526
+ // Show most severe alert
527
+ const severe = alerts.find(a => a.severity === 'Warning')
528
+ || alerts.find(a => a.severity === 'Watch')
529
+ || alerts[0]
530
+
531
+ return (
532
+ <div className={`alert-banner alert-${severe.severity.toLowerCase()}`}>
533
+ <strong>{severe.severity}:</strong> {severe.title}
534
+ </div>
535
+ )
375
536
  }
376
537
  ```
377
538
 
539
+ ## Weather Codes
540
+
541
+ The `weatherCode` field uses WeatherBit condition codes. For the complete list, see: https://www.weatherbit.io/api/codes
542
+
543
+ Common codes:
544
+ - 800: Clear sky
545
+ - 801-804: Clouds (few to overcast)
546
+ - 500-531: Rain
547
+ - 600-623: Snow
548
+ - 200-233: Thunderstorm
549
+
378
550
  ## Tips
379
551
 
380
- 1. **Cache results** - Weather doesn't change rapidly; refresh every 10-30 minutes
381
- 2. **Handle loading** - Show skeleton or spinner while fetching
382
- 3. **Show stale data** - Display last known data while refreshing
383
- 4. **Validate city** - Weather API may fail for invalid city names
384
- 5. **Use coordinates** - More reliable than city names for precise locations
552
+ 1. **Store cityId, not city name** - The cityId is required for all weather methods
553
+ 2. **Refresh appropriately** - Current conditions every 15 minutes, forecasts every 30-60 minutes
554
+ 3. **Show stale data while refreshing** - Don't clear the display during background updates
555
+ 4. **Handle no city selected** - Show a helpful message directing users to Settings
556
+ 5. **Use both units when helpful** - Digital signage often serves diverse audiences
557
+ 6. **Check for alerts** - Weather warnings are important for public displays
558
+ 7. **Use partOfDay** - Adjust UI styling for day vs night conditions