@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.
- package/CHANGELOG.md +25 -0
- package/dist/commands/auth.js +4 -4
- package/dist/commands/init.js +90 -42
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +208 -0
- package/dist/index.js +2 -0
- package/dist/plugins/math-tools.d.ts +2 -0
- package/dist/plugins/math-tools.js +18 -0
- package/dist/services/api-client.d.ts +18 -0
- package/dist/services/api-client.js +70 -0
- package/dist/services/archiver.d.ts +4 -0
- package/dist/services/archiver.js +65 -0
- package/dist/services/build-poller.d.ts +10 -0
- package/dist/services/build-poller.js +63 -0
- package/dist/services/cli-config.d.ts +6 -0
- package/dist/services/cli-config.js +23 -0
- package/dist/services/generate-application.d.ts +2 -1
- package/dist/services/generate-application.js +31 -32
- package/dist/services/project-config.d.ts +24 -0
- package/dist/services/project-config.js +51 -0
- package/dist/services/run-server.js +29 -73
- package/dist/types/api.d.ts +44 -0
- package/dist/types/api.js +1 -0
- package/dist/types/applications.d.ts +44 -0
- package/dist/types/applications.js +1 -0
- package/dist/utils/ansi.d.ts +10 -0
- package/dist/utils/ansi.js +10 -0
- package/dist/utils/path-utils.d.ts +55 -0
- package/dist/utils/path-utils.js +99 -0
- package/package.json +4 -2
- package/templates/vite-react-typescript/CLAUDE.md +6 -5
- package/templates/vite-react-typescript/_claude/settings.local.json +2 -1
- package/templates/vite-react-typescript/_claude/skills/tos-render-design/SKILL.md +304 -12
- package/templates/vite-react-typescript/_claude/skills/tos-requirements/SKILL.md +367 -130
- 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
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
{ postalCode: '10001' }
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### By Coordinates
|
|
16
|
+
## Quick Reference
|
|
60
17
|
|
|
61
18
|
```typescript
|
|
62
|
-
{
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Units
|
|
19
|
+
import { weather } from '@telemetryos/sdk'
|
|
66
20
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
//
|
|
72
|
-
{
|
|
73
|
-
```
|
|
25
|
+
// 2. Get current conditions
|
|
26
|
+
const conditions = await weather.getConditions({ cityId })
|
|
74
27
|
|
|
75
|
-
|
|
28
|
+
// 3. Get daily forecast (up to 16 days)
|
|
29
|
+
const daily = await weather.getDailyForecast({ cityId, days: 7 })
|
|
76
30
|
|
|
77
|
-
|
|
31
|
+
// 4. Get hourly forecast (up to 120 hours)
|
|
32
|
+
const hourly = await weather.getHourlyForecast({ cityId, hours: 24 })
|
|
78
33
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
Cities are selected in Settings, weather is displayed in Render. This is the standard workflow.
|
|
147
41
|
|
|
148
|
-
###
|
|
42
|
+
### Store Hook
|
|
149
43
|
|
|
150
44
|
```typescript
|
|
151
45
|
// hooks/store.ts
|
|
152
46
|
import { createUseInstanceStoreState } from '@telemetryos/sdk/react'
|
|
153
47
|
|
|
154
|
-
|
|
155
|
-
export const
|
|
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 {
|
|
66
|
+
import { useCityIdState, useCityNameState } from '../hooks/store'
|
|
168
67
|
|
|
169
68
|
export default function Settings() {
|
|
170
|
-
const [
|
|
171
|
-
const [
|
|
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="
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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 {
|
|
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 [
|
|
223
|
-
const [
|
|
156
|
+
const [isLoadingId, cityId] = useCityIdState()
|
|
157
|
+
const [isLoadingName, cityName] = useCityNameState()
|
|
224
158
|
|
|
225
|
-
const [
|
|
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 (
|
|
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
|
|
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
|
|
257
|
-
const interval = setInterval(fetchWeather,
|
|
182
|
+
// Refresh every 15 minutes
|
|
183
|
+
const interval = setInterval(fetchWeather, 15 * 60 * 1000)
|
|
258
184
|
return () => clearInterval(interval)
|
|
259
|
-
}, [
|
|
185
|
+
}, [cityId, isLoadingId])
|
|
260
186
|
|
|
261
187
|
// Loading states
|
|
262
|
-
if (
|
|
263
|
-
if (!
|
|
264
|
-
if (loading && !
|
|
265
|
-
if (error && !
|
|
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>{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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(
|
|
303
|
-
console.log(`${
|
|
274
|
+
forecast.data.forEach(hour => {
|
|
275
|
+
console.log(`${hour.forecastTimeLocal}: ${hour.temperatureC}°C`)
|
|
276
|
+
console.log(` ${hour.weatherDescription}`)
|
|
304
277
|
})
|
|
305
278
|
```
|
|
306
279
|
|
|
307
|
-
|
|
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
|
-
|
|
437
|
+
### WeatherAlert
|
|
310
438
|
|
|
311
439
|
```typescript
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
356
|
-
|
|
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
|
-
##
|
|
499
|
+
## Weather Alerts Example
|
|
361
500
|
|
|
362
501
|
```typescript
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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. **
|
|
381
|
-
2. **
|
|
382
|
-
3. **Show stale data** -
|
|
383
|
-
4. **
|
|
384
|
-
5. **Use
|
|
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
|