Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/environment

src/snapshot.ts

95 lines · 3.0 KB · TypeScript
import airQualityLookup from './air-quality.ts'
import moonLookup from './moon.ts'
import sunLookup from './sun.ts'
import weatherLookup from './weather.ts'
import { resolveWeatherLocation } from './weather-geocode.ts'

export type EnvironmentSnapshotParams = {
	location?: string
	latitude?: number
	longitude?: number
	label?: string
	date?: string
	units?: 'metric' | 'imperial'
	/** Set false to skip a section and save the upstream calls. All default to true. */
	include?: {
		weather?: boolean
		airQuality?: boolean
		sun?: boolean
		moon?: boolean
	}
}

function sectionError(error: unknown): { error: string } {
	return { error: error instanceof Error ? error.message : String(error) }
}

/**
 * One-call outdoor snapshot combining weather, air quality, sun times, and
 * moon phase for a single location. Sections degrade independently: a failed
 * upstream lookup yields `{ error }` for that section instead of throwing.
 */
export async function environmentSnapshot(
	params: EnvironmentSnapshotParams = {},
) {
	const input = typeof params === 'object' && params ? params : {}
	const include = input.include || {}
	// Resolve the location once so all sections agree on coordinates and we
	// only hit the geocoder a single time.
	const resolvedLocation = await resolveWeatherLocation(input)
	const shared = {
		latitude: resolvedLocation.latitude,
		longitude: resolvedLocation.longitude,
		label: resolvedLocation.displayName,
		date: input.date,
	}
	const [weather, airQuality, sun, moon] = await Promise.all([
		include.weather === false
			? Promise.resolve(null)
			: weatherLookup({ ...shared, units: input.units }).catch(sectionError),
		include.airQuality === false
			? Promise.resolve(null)
			: airQualityLookup(shared).catch(sectionError),
		include.sun === false
			? Promise.resolve(null)
			: sunLookup(shared).catch(sectionError),
		include.moon === false
			? Promise.resolve(null)
			: moonLookup({ date: input.date }).catch(sectionError),
	])
	const summaryLines: string[] = [
		'Environment snapshot for ' + resolvedLocation.displayName,
	]
	for (const section of [weather, airQuality, sun, moon]) {
		if (!section) continue
		if ('error' in section) continue
		if (typeof (section as { summary?: unknown }).summary === 'string') {
			summaryLines.push((section as { summary: string }).summary)
		}
	}
	const failed = []
	if (weather && 'error' in weather) failed.push('weather: ' + weather.error)
	if (airQuality && 'error' in airQuality) {
		failed.push('airQuality: ' + airQuality.error)
	}
	if (sun && 'error' in sun) failed.push('sun: ' + sun.error)
	if (moon && 'error' in moon) failed.push('moon: ' + moon.error)
	if (failed.length > 0) {
		summaryLines.push('Failed sections: ' + failed.join('; '))
	}
	return {
		location: {
			displayName: resolvedLocation.displayName,
			latitude: resolvedLocation.latitude,
			longitude: resolvedLocation.longitude,
			source: resolvedLocation.source,
		},
		weather,
		airQuality,
		sun,
		moon,
		summary: summaryLines.join('\n\n'),
	}
}

export default environmentSnapshot