← Public packages
@kentcdodds/venstar
Typed Venstar thermostat helpers for climate summaries, comfort setpoints, away mode, schedules, sensors, and runtimes.
src/index.ts
127 lines · 8.1 KB · TypeScriptimport { kody } from 'kody:runtime'
export type ThermostatMode = 'off' | 'heat' | 'cool' | 'auto'
export type FanMode = 'auto' | 'on'
export type ThermostatLookupParams = { /** Saved thermostat name or IP address. Omit only when the low-level connector has a default. */ thermostat?: string }
export type ClimateSummary = { ok: true; thermostats: unknown }
export type ThermostatInfoResult = { ok: true; thermostat?: string; info: unknown }
export type ThermostatDataResult = { ok: true; thermostat?: string; data: unknown }
export type SetComfortParams = ThermostatLookupParams & { /** Desired HVAC mode. */ mode?: ThermostatMode; /** Desired fan mode. */ fan?: FanMode; /** Heat setpoint in Fahrenheit. */ heattemp?: number; /** Cool setpoint in Fahrenheit. */ cooltemp?: number; /** Minimum gap required between heat and cool in auto mode. Defaults to 2. */ minAutoGap?: number }
export type SetAutoRangeParams = ThermostatLookupParams & { /** Heat setpoint in Fahrenheit. */ heattemp: number; /** Cool setpoint in Fahrenheit. */ cooltemp: number; /** Desired fan mode. */ fan?: FanMode; /** Minimum gap required between heat and cool. Defaults to 2. */ minAutoGap?: number }
export type SetAwayParams = ThermostatLookupParams & { /** true for away mode, false for home mode. */ away: boolean }
export type SetScheduleParams = ThermostatLookupParams & { /** true to enable the local thermostat schedule, false to disable it. */ schedule: boolean }
export type ThermostatMutationResult = { ok: true; thermostat?: string; request: Record<string, unknown>; result: unknown } | { ok: false; error: string }
export type VenstarThermostatsNamespace = { listThermostats: typeof listThermostats; summarizeClimate: typeof summarizeClimate; getInfo: typeof getInfo; getSensors: typeof getSensors; getRuntimes: typeof getRuntimes; setComfort: typeof setComfort; setAutoRange: typeof setAutoRange; setAway: typeof setAway; setSchedule: typeof setSchedule }
const MODE_TO_NUMBER: Record<ThermostatMode, number> = { off: 0, heat: 1, cool: 2, auto: 3 }
const FAN_TO_NUMBER: Record<FanMode, number> = { auto: 0, on: 1 }
function normalizeText(value: unknown): string { return typeof value === 'string' ? value.trim().toLowerCase() : '' }
function normalizeThermostat(value: unknown): string | undefined { return typeof value === 'string' && value.trim() ? value.trim() : undefined }
function assertTemp(name: string, value: unknown): number | undefined {
if (value === undefined) return undefined
const numeric = Number(value)
if (!Number.isFinite(numeric) || numeric < 40 || numeric > 99) throw new Error(name + ' must be a Fahrenheit setpoint between 40 and 99.')
return numeric
}
function modeNumber(mode?: ThermostatMode): number | undefined { return mode ? MODE_TO_NUMBER[mode] : undefined }
function fanNumber(fan?: FanMode): number | undefined { return fan ? FAN_TO_NUMBER[fan] : undefined }
function validateAutoRange(heattemp: number, cooltemp: number, minAutoGap = 2): string | null { return cooltemp > heattemp + minAutoGap ? null : 'In auto mode, cooltemp must be greater than heattemp plus minAutoGap.' }
/** List managed Venstar thermostats with saved names/IPs and status summaries. */
export async function listThermostats(): Promise<unknown> { return await kody.mcp['home'].venstar_list_thermostats({}) }
/** Read all managed thermostats as a compact climate summary. */
export async function summarizeClimate(): Promise<ClimateSummary> { return { ok: true, thermostats: await listThermostats() } }
/** Fetch /query/info for one Venstar thermostat by saved name or IP. */
export async function getInfo(params: ThermostatLookupParams = {}): Promise<ThermostatInfoResult> {
const thermostat = normalizeThermostat(params.thermostat)
const info = await kody.mcp['home'].venstar_get_thermostat_info({ thermostat })
return { ok: true, thermostat, info }
}
/** Fetch /query/sensors for one Venstar thermostat by saved name or IP. */
export async function getSensors(params: ThermostatLookupParams = {}): Promise<ThermostatDataResult> {
const thermostat = normalizeThermostat(params.thermostat)
const data = await kody.mcp['home'].venstar_get_thermostat_sensors({ thermostat })
return { ok: true, thermostat, data }
}
/** Fetch /query/runtimes for one Venstar thermostat by saved name or IP. */
export async function getRuntimes(params: ThermostatLookupParams = {}): Promise<ThermostatDataResult> {
const thermostat = normalizeThermostat(params.thermostat)
const data = await kody.mcp['home'].venstar_get_thermostat_runtimes({ thermostat })
return { ok: true, thermostat, data }
}
/** Set Venstar mode, fan, heat setpoint, and/or cool setpoint with typed strings. */
export async function setComfort(params: SetComfortParams): Promise<ThermostatMutationResult> {
try {
const thermostat = normalizeThermostat(params.thermostat)
const heattemp = assertTemp('heattemp', params.heattemp)
const cooltemp = assertTemp('cooltemp', params.cooltemp)
if (params.mode === 'auto' && heattemp !== undefined && cooltemp !== undefined) {
const error = validateAutoRange(heattemp, cooltemp, params.minAutoGap)
if (error) return { ok: false, error }
}
const request: Record<string, unknown> = { thermostat }
const mode = modeNumber(params.mode)
const fan = fanNumber(params.fan)
if (mode !== undefined) request.mode = mode
if (fan !== undefined) request.fan = fan
if (heattemp !== undefined) request.heattemp = heattemp
if (cooltemp !== undefined) request.cooltemp = cooltemp
const result = await kody.mcp['home'].venstar_control_thermostat(request)
return { ok: true, thermostat, request, result }
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
/** Set auto mode with validated heat/cool setpoints. */
export async function setAutoRange(params: SetAutoRangeParams): Promise<ThermostatMutationResult> {
const missing: Array<string> = []
if (params.heattemp === undefined) missing.push('heattemp')
if (params.cooltemp === undefined) missing.push('cooltemp')
if (missing.length) return { ok: false, error: missing.join(' and ') + (missing.length > 1 ? ' are' : ' is') + ' required for setAutoRange.' }
return await setComfort({ ...params, mode: 'auto' })
}
/** Toggle Venstar away/home mode. */
export async function setAway(params: SetAwayParams): Promise<ThermostatMutationResult> {
try {
if (typeof params.away !== 'boolean') throw new Error('away must be an explicit boolean: true for away mode, false for home mode.')
const thermostat = normalizeThermostat(params.thermostat)
const request = { thermostat, away: params.away ? 1 : 0 }
const result = await kody.mcp['home'].venstar_set_thermostat_settings(request)
return { ok: true, thermostat, request, result }
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
/** Enable or disable the thermostat's local schedule. */
export async function setSchedule(params: SetScheduleParams): Promise<ThermostatMutationResult> {
try {
if (typeof params.schedule !== 'boolean') throw new Error('schedule must be an explicit boolean: true to enable the local schedule, false to disable it.')
const thermostat = normalizeThermostat(params.thermostat)
const request = { thermostat, schedule: params.schedule ? 1 : 0 }
const result = await kody.mcp['home'].venstar_set_thermostat_settings(request)
return { ok: true, thermostat, request, result }
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
/**
* Default callable for `kody:@kentcdodds/venstar`; returns the typed Venstar helper namespace.
*
* @example
* import venstarThermostats from 'kody:@kentcdodds/venstar'
* const venstar = await venstarThermostats()
* const info = await venstar.getInfo({ thermostat: 'apartment' })
* // => { ok: true, thermostat: 'apartment', info: { mode: 3, heattemp: 68, cooltemp: 74, ... } }
*/
export default async function venstarThermostats(): Promise<VenstarThermostatsNamespace> {
return { listThermostats, summarizeClimate, getInfo, getSensors, getRuntimes, setComfort, setAutoRange, setAway, setSchedule }
}