← Public packages
@kentcdodds/hydrawise
Server-side Hunter Hydrawise controller and zone helpers backed by the hydrawiseApiKey secret.
src/weekday-status.js
140 lines · 4.2 KB · JavaScriptimport postMessage from 'kody:@kentcdodds/discord/post-message'
import { getControllers, getStatus, isSoftSkipError } from './index.js'
export { isSoftSkipError }
const REPORT_CHANNEL_ID = '1498718302237757654'
/**
* Weekday Hydrawise status line for Discord #reporting. Read-only: controllers,
* rain/sensors, and next zone run. Never runs or suspends zones. Soft-skips
* Hydrawise outages and 429 rate limits instead of error-spamming.
*
* @param input
* @param [input.dryRun] - Format the status without posting Discord.
* @returns Status snapshot and optional Discord post.
*
* @example
* import weekdayStatus from 'kody:@kentcdodds/hydrawise/weekday-status'
* const result = await weekdayStatus({ dryRun: true })
* // => { ok: true, dryRun: true, content: '🌱 **Hydrawise** · ...' }
*/
export default async function weekdayStatus(input = {}) {
try {
return await runWeekdayStatus(input)
} catch (error) {
if (isSoftSkipError(error)) {
return {
ok: true,
skipped: true,
reason: 'soft-skip',
error: error instanceof Error ? error.message : String(error),
}
}
throw error
}
}
async function runWeekdayStatus(input = {}) {
const [controllers, status] = await Promise.all([getControllers(), getStatus()])
const content = formatStatus(controllers, status)
if (input.dryRun) {
return {
ok: true,
dryRun: true,
content,
controller: summarizeController(controllers),
rain: rainSummary(status),
nextRun: nextRunSummary(status),
posted: false,
}
}
const post = await postMessage({ channelId: REPORT_CHANNEL_ID, content })
const messageId = post?.id || post?.message?.id || post?.messageId || null
return {
ok: true,
posted: true,
messageId,
content,
controller: summarizeController(controllers),
rain: rainSummary(status),
nextRun: nextRunSummary(status),
post,
}
}
function summarizeController(controllers) {
const current =
controllers.controllers?.find(
(c) => c.id === controllers.currentControllerId,
) || controllers.controllers?.[0]
return {
name: controllers.currentControllerName || current?.name || 'Hydrawise',
id: controllers.currentControllerId || current?.id,
lastContact: current?.lastContact,
status: current?.status,
count: controllers.controllers?.length || 0,
}
}
function rainSummary(status) {
const sensors = Array.isArray(status.sensors) ? status.sensors : []
const rain = sensors.find((sensor) => {
const name = String(sensor.name || sensor.type || '').toLowerCase()
return name.includes('rain') || Number(sensor.type) === 1
})
if (!rain) {
return { present: sensors.length > 0, wet: null, label: 'rain unknown' }
}
const input = rain.input ?? rain.active ?? rain.off
const wet = input === 1 || input === true || String(rain.timer || '') === '1'
return {
present: true,
wet,
name: rain.name || 'Rain',
label: wet ? 'rain: wet / delaying' : 'rain: dry',
raw: { name: rain.name, type: rain.type, input: rain.input },
}
}
function nextRunSummary(status) {
const zones = Array.isArray(status.zones) ? status.zones : []
const upcoming = zones
.filter(
(zone) =>
typeof zone.nextRunInSeconds === 'number' &&
zone.nextRunInSeconds >= 0 &&
zone.nextRunInSeconds < 7 * 24 * 3600,
)
.sort((a, b) => a.nextRunInSeconds - b.nextRunInSeconds)
const next = upcoming[0]
if (!next) return { label: 'no run scheduled' }
return {
name: next.name,
number: next.number,
inSeconds: next.nextRunInSeconds,
timeLabel: next.nextRunTimeLabel,
durationSeconds: next.runDurationSeconds,
label: next.nextRunTimeLabel
? `${next.name} ${next.nextRunTimeLabel}`
: `${next.name} in ${Math.round(next.nextRunInSeconds / 60)}m`,
}
}
function formatStatus(controllers, status) {
const controller = summarizeController(controllers)
const rain = rainSummary(status)
const next = nextRunSummary(status)
const watering = (status.zones || []).filter(
(zone) => zone.nextRunTimeLabel === 'Running now' || zone.nextRunInSeconds === 0,
)
const wateringBit = watering.length
? `watering ${watering.map((z) => z.name).join(', ')}`
: null
return [
`🌱 **Hydrawise** · ${controller.name} · ${rain.label} · next ${next.label}`,
wateringBit ? `Now: ${wateringBit}` : null,
]
.filter(Boolean)
.join('\n')
}