Skip to content

Commit c29defe

Browse files
feat: weather integration type, onWeatherGet handler and pivot format (spec B.18) (#19)
* feat: weather integration type — onWeatherGet handler and pivot format (spec B.18) Implement the "weather" integration type shipped by Gladys (B.18, GladysAssistant/Gladys#2738): a weather provider answers the core's external-integration.weather.get command with the pivot weather format, acked back as data.weather under the 15 s deadline. - WEATHER_GET WebSocket message type, routed to the new onWeatherGet handler ((options) => Promise<pivot weather>), auto-acked like every command - WEATHER_CONDITIONS and WEATHER_ALERT_SEVERITIES constants exported (CJS + ESM), plus the core-side bounds of the pivot format - TypeScript typings of the whole pivot format (WeatherGetOptions, WeatherPayload, hourly/daily forecasts, CAP-style alerts) - README: onWeatherGet handler row + a dedicated "Weather providers" section (unit systems, condition enum, normalization bounds) - tests: WS command acks (success, us units, failure, not implemented) and constants, + compile-time typing checks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPXiFG8AQAUprvrRNCtaKF * feat: sync the weather pivot format with Gladys — finer conditions, is_day, typed alerts Mirror the Gladys-side extension of the B.18 pivot weather format (GladysAssistant/Gladys#2738, follow-up commit): - condition enum gains partly-cloudy, pouring and hail; night stays accepted but is deprecated for providers (send the real condition plus is_day: false — a rainy night stays rain) - is_day optional strict boolean on the current conditions and each hours entry, driving the day/night rendering variant - alerts gain an optional phenomenon type (WEATHER_ALERT_TYPES: wind, rain, flood, thunderstorm, snow, heat, cold, avalanche, coastal, fog), exported CJS + ESM and typed; an invalid type is dropped by the core, the alert is kept Typings, README section and tests updated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPXiFG8AQAUprvrRNCtaKF * feat: weather provider images and freshness nudge (spec B.18 points 5-6) Mirror the latest extension of the Gladys weather type (GladysAssistant/Gladys#2738): alert scene triggers are core-owned (zero integration contract), but two new SDK surfaces ship with them: - onWeatherGetImage(cb) — the core requests a provider image declared in the pivot's new `images` metadata (<= 3 entries of { key, label? }); resolve the raw base64 of a PNG/JPEG (<= 500 KB decoded), acked as data.image under 15 s, validated and cached 10 min by the core - requestWeatherRefresh() — fire-and-forget freshness nudge over the new external-integration.weather.refresh message: "re-pull me now and re-evaluate the alert scene triggers"; no payload, no ack, rate-limited core-side (1/min), dropped silently while disconnected - alert description bound raised 2000 -> 5000 chars (CAP bulletins run long), image bounds mirrored in constants Typings (WeatherImage, images field, new message types), README (handler and method rows + provider images / freshness nudge subsections) and tests updated accordingly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPXiFG8AQAUprvrRNCtaKF --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 800bd7d commit c29defe

8 files changed

Lines changed: 838 additions & 16 deletions

File tree

README.md

Lines changed: 108 additions & 15 deletions
Large diffs are not rendered by default.

esm/index.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export const {
88
DEVICE_FEATURE_TYPES,
99
DEVICE_FEATURE_UNITS,
1010
DEVICE_TRANSPORTS,
11+
WEATHER_CONDITIONS,
12+
WEATHER_ALERT_SEVERITIES,
13+
WEATHER_ALERT_TYPES,
1114
createLogger,
1215
logger,
1316
} = sdk;

index.d.ts

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,197 @@ export interface LinkedContact {
228228
user: LinkedUser | null;
229229
}
230230

231+
/**
232+
* Conditions of the pivot weather format (contract B.18). Anything else is
233+
* coerced to 'unknown' by the Gladys core. 'night' is deprecated for
234+
* providers: send the real condition plus `is_day: false` instead (a rainy
235+
* night stays 'rain').
236+
*/
237+
export type WeatherCondition =
238+
| 'clear'
239+
| 'partly-cloudy'
240+
| 'cloud'
241+
| 'fog'
242+
| 'drizzle'
243+
| 'rain'
244+
| 'pouring'
245+
| 'sleet'
246+
| 'hail'
247+
| 'snow'
248+
| 'thunderstorm'
249+
| 'wind'
250+
| 'night'
251+
| 'unknown';
252+
253+
/**
254+
* CAP-style severity of a weather alert (contract B.18) — Common Alerting
255+
* Protocol, never one provider's scale (Météo France vigilance: yellow →
256+
* moderate, orange → severe, red → extreme).
257+
*/
258+
export type WeatherAlertSeverity = 'minor' | 'moderate' | 'severe' | 'extreme';
259+
260+
/**
261+
* Phenomenon type of a weather alert (contract B.18), generalized from the
262+
* Météo France vigilance phenomena, the MeteoAlarm awareness types and the
263+
* NWS event catalog (vent violent → wind, pluie-inondation → rain, orages →
264+
* thunderstorm, inondation → flood, neige-verglas → snow, canicule → heat,
265+
* grand froid → cold, avalanches → avalanche, vagues-submersion → coastal).
266+
* Optional metadata: an invalid type is dropped by the core, the alert is
267+
* kept and rendered from its `event` text alone.
268+
*/
269+
export type WeatherAlertType =
270+
'wind' | 'rain' | 'flood' | 'thunderstorm' | 'snow' | 'heat' | 'cold' | 'avalanche' | 'coastal' | 'fog';
271+
272+
/** A date of the pivot weather format: ISO string, timestamp or Date. */
273+
export type WeatherDate = string | number | Date;
274+
275+
/**
276+
* Unit system requested by Gladys (contract B.18): the user's preference.
277+
* Return values in that system — °C, m/s, hPa, mm, km for 'metric'; °F,
278+
* mph, in, mi for 'us'.
279+
*/
280+
export type WeatherUnits = 'metric' | 'us';
281+
282+
/** Options of a weather request (contract B.18), as received by onWeatherGet. */
283+
export interface WeatherGetOptions {
284+
latitude: number;
285+
longitude: number;
286+
/** Preferred language of the user, e.g. 'en', 'fr'. */
287+
language: string;
288+
units: WeatherUnits;
289+
}
290+
291+
/** One hourly forecast entry of the pivot weather format (≤ 24 kept by Gladys). */
292+
export interface WeatherHourForecast {
293+
temperature: number;
294+
weather: WeatherCondition;
295+
datetime: WeatherDate;
296+
apparent_temperature?: number;
297+
/** Percentage, 0-100. */
298+
humidity?: number;
299+
pressure?: number;
300+
wind_speed?: number;
301+
/** Degrees, 0-360. */
302+
wind_direction?: number;
303+
wind_gust?: number;
304+
/** Percentage, 0-100. */
305+
cloud_cover?: number;
306+
/** Precipitation over the hour (mm for metric, in for us). */
307+
precipitation?: number;
308+
/** Percentage, 0-100. */
309+
precipitation_probability?: number;
310+
uv_index?: number;
311+
/**
312+
* Day/night rendering variant (strict boolean: anything else is dropped by
313+
* the core, never coerced). Absent → rendered as day.
314+
*/
315+
is_day?: boolean;
316+
}
317+
318+
/**
319+
* One daily forecast entry of the pivot weather format (≤ 8 kept by Gladys).
320+
* `days` may or may not include the current day: consumers filter by
321+
* calendar date — a provider never has to lead with today.
322+
*/
323+
export interface WeatherDayForecast {
324+
temperature_min: number;
325+
temperature_max: number;
326+
datetime: WeatherDate;
327+
weather?: WeatherCondition;
328+
/** Percentage, 0-100. */
329+
humidity?: number;
330+
wind_speed?: number;
331+
/** Degrees, 0-360. */
332+
wind_direction?: number;
333+
wind_gust?: number;
334+
/** Precipitation over the day (mm for metric, in for us). */
335+
precipitation?: number;
336+
/** Percentage, 0-100. */
337+
precipitation_probability?: number;
338+
uv_index?: number;
339+
sunrise?: WeatherDate;
340+
sunset?: WeatherDate;
341+
}
342+
343+
/** One weather alert of the pivot weather format (≤ 10 kept by Gladys). */
344+
export interface WeatherAlert {
345+
severity: WeatherAlertSeverity;
346+
/** Short name of the event (≤ 100 characters), e.g. 'Orages violents'. */
347+
event: string;
348+
/**
349+
* Phenomenon type, so the core can translate and iconify the alert. An
350+
* invalid type is dropped by the core; the alert is kept and rendered
351+
* from its `event` text alone.
352+
*/
353+
type?: WeatherAlertType;
354+
/** Longer description (≤ 5000 characters — CAP descriptions run long). */
355+
description?: string;
356+
start?: WeatherDate;
357+
end?: WeatherDate;
358+
}
359+
360+
/**
361+
* Metadata of one provider image of the pivot weather format (contract
362+
* B.18: vigilance map, rain radar, satellite view… — ≤ 3 kept by Gladys).
363+
* Metadata only: the bytes travel on demand through the onWeatherGetImage
364+
* handler, never in the weather payload.
365+
*/
366+
export interface WeatherImage {
367+
/** Image key, matching `^[a-z0-9][a-z0-9-]{0,31}$` — unique per payload. */
368+
key: string;
369+
/**
370+
* Display label of the image, keyed by language code (values ≤ 50
371+
* characters). Absent → the widget shows the raw key.
372+
*/
373+
label?: Record<string, string>;
374+
}
375+
376+
/**
377+
* The pivot weather format resolved by onWeatherGet (contract B.18), acked
378+
* back to Gladys as `data.weather`. Values must be in the requested unit
379+
* system (`WeatherGetOptions.units`); percentages are 0-100. The payload is
380+
* normalized and bounded by the Gladys core: unknown fields are dropped,
381+
* percentages clamped, unknown conditions coerced to 'unknown', arrays
382+
* capped (24 hours, 8 days, 10 alerts).
383+
*/
384+
export interface WeatherPayload {
385+
temperature: number;
386+
weather: WeatherCondition;
387+
datetime: WeatherDate;
388+
/** Feels-like temperature. */
389+
apparent_temperature?: number;
390+
/** Percentage, 0-100. */
391+
humidity?: number;
392+
pressure?: number;
393+
dew_point?: number;
394+
wind_speed?: number;
395+
/** Degrees, 0-360. */
396+
wind_direction?: number;
397+
wind_gust?: number;
398+
/** km for metric, mi for us. */
399+
visibility?: number;
400+
/** Percentage, 0-100. */
401+
cloud_cover?: number;
402+
uv_index?: number;
403+
sunrise?: WeatherDate;
404+
sunset?: WeatherDate;
405+
/**
406+
* Day/night rendering variant (strict boolean: anything else is dropped by
407+
* the core, never coerced). Absent → rendered as day. `weather` keeps the
408+
* meteorology, `is_day` drives the day/night icon variant — preferred over
409+
* the deprecated 'night' condition.
410+
*/
411+
is_day?: boolean;
412+
hours?: WeatherHourForecast[];
413+
days?: WeatherDayForecast[];
414+
alerts?: WeatherAlert[];
415+
/**
416+
* Provider images declared as metadata (≤ 3): the bytes are fetched on
417+
* demand through onWeatherGetImage, never carried in the payload.
418+
*/
419+
images?: WeatherImage[];
420+
}
421+
231422
/**
232423
* Modes of a webhook declared in the manifest `webhooks` field (contract
233424
* B.17): 'fire_and_forget' — the third party only awaits an acknowledgment
@@ -929,6 +1120,9 @@ export declare const WEBSOCKET_MESSAGE_TYPES: {
9291120
OAUTH_CALLBACK: string;
9301121
ACTION_RUN: string;
9311122
CAMERA_GET_IMAGE: string;
1123+
WEATHER_GET: string;
1124+
WEATHER_GET_IMAGE: string;
1125+
WEATHER_REFRESH: string;
9321126
MESSAGE_SEND: string;
9331127
WEBHOOK_RECEIVED: string;
9341128
WEBHOOK_REQUEST: string;
@@ -944,6 +1138,47 @@ export declare const DEVICE_TRANSPORTS: {
9441138
readonly UNREACHABLE: 'unreachable';
9451139
};
9461140

1141+
/** Conditions of the pivot weather format (contract B.18). */
1142+
export declare const WEATHER_CONDITIONS: {
1143+
readonly CLEAR: 'clear';
1144+
readonly PARTLY_CLOUDY: 'partly-cloudy';
1145+
readonly CLOUD: 'cloud';
1146+
readonly FOG: 'fog';
1147+
readonly DRIZZLE: 'drizzle';
1148+
readonly RAIN: 'rain';
1149+
readonly POURING: 'pouring';
1150+
readonly SLEET: 'sleet';
1151+
readonly HAIL: 'hail';
1152+
readonly SNOW: 'snow';
1153+
readonly THUNDERSTORM: 'thunderstorm';
1154+
readonly WIND: 'wind';
1155+
/** Deprecated for providers: send the real condition + `is_day: false`. */
1156+
readonly NIGHT: 'night';
1157+
readonly UNKNOWN: 'unknown';
1158+
};
1159+
1160+
/** CAP-style severities of the weather alerts (contract B.18). */
1161+
export declare const WEATHER_ALERT_SEVERITIES: {
1162+
readonly MINOR: 'minor';
1163+
readonly MODERATE: 'moderate';
1164+
readonly SEVERE: 'severe';
1165+
readonly EXTREME: 'extreme';
1166+
};
1167+
1168+
/** Phenomenon types of the weather alerts (contract B.18). */
1169+
export declare const WEATHER_ALERT_TYPES: {
1170+
readonly WIND: 'wind';
1171+
readonly RAIN: 'rain';
1172+
readonly FLOOD: 'flood';
1173+
readonly THUNDERSTORM: 'thunderstorm';
1174+
readonly SNOW: 'snow';
1175+
readonly HEAT: 'heat';
1176+
readonly COLD: 'cold';
1177+
readonly AVALANCHE: 'avalanche';
1178+
readonly COASTAL: 'coastal';
1179+
readonly FOG: 'fog';
1180+
};
1181+
9471182
/**
9481183
* Client of the Gladys host API + integration WebSocket. See the README for a
9491184
* complete example.
@@ -1192,6 +1427,42 @@ export declare class GladysIntegration extends EventEmitter {
11921427
*/
11931428
onSendMessage(callback: (contact: MessageContact, message: OutgoingMessage) => void | Promise<void>): void;
11941429

1430+
/**
1431+
* Handler called when Gladys asks a weather integration (manifest
1432+
* `type: "weather"`, contract B.18) for the weather (auto-acked) — the
1433+
* dashboard weather widget or the chat assistant needs it. `options.units`
1434+
* is the requesting user's preference ('metric' or 'us'): return values in
1435+
* that unit system. Resolve the pivot weather format: it is acked back as
1436+
* `data.weather` — awaited under 15 s (not the standard 5 s) so a fresh
1437+
* third-party API call fits — then normalized and bounded by the Gladys
1438+
* core. Throwing acks the command as failed, and the Gladys provider loop
1439+
* falls through to the next provider.
1440+
*/
1441+
onWeatherGet(callback: (options: WeatherGetOptions) => WeatherPayload | Promise<WeatherPayload>): void;
1442+
1443+
/**
1444+
* Handler called when Gladys asks a weather integration for one of the
1445+
* provider images declared in the pivot's `images` metadata (contract
1446+
* B.18: vigilance map, rain radar… — auto-acked). Registered once for all
1447+
* keys; resolve the RAW base64 (no `data:` URI prefix) of the requested
1448+
* image — a PNG or JPEG of at most 500 KB decoded (magic numbers and size
1449+
* checked by the core, which caches the validated image 10 minutes and
1450+
* serves it from its own origin). The ack is awaited under 15 s (not the
1451+
* standard 5 s) so a fresh fetch at the provider fits.
1452+
*/
1453+
onWeatherGetImage(callback: (key: string) => string | Promise<string>): void;
1454+
1455+
/**
1456+
* Send a freshness nudge to Gladys (contract B.18, weather integrations,
1457+
* "trigger, not data"): ask the core to re-pull the weather NOW — through
1458+
* the normal onWeatherGet path — and re-evaluate the weather-alert scene
1459+
* triggers, instead of waiting for the 30-minute scheduled check. Carries
1460+
* no data, expects no answer (fire-and-forget). Rate-limited by the core
1461+
* to 1 per minute per integration, silently dropped beyond — and dropped
1462+
* silently too while the WebSocket is disconnected.
1463+
*/
1464+
requestWeatherRefresh(): void;
1465+
11951466
/**
11961467
* Handler of ONE webhook declared in the manifest `webhooks` field
11971468
* (contract B.17): third-party events pushed from the Internet, relayed by

0 commit comments

Comments
 (0)