(also google TTS node)
I'm still at the shallow end of Home Automation.
I have written a nice bit of code with nodes to do a visual display of the weather at that point in time.
I'm expanding with the TTS node and wanting a FORECAST of weather too.
So in the morning, I can get a bit of an idea of the day's weather.
(putting aside the 90% inaccurate history of weather forecasts)
So I got some help and this is what I've got.
I have two options here: Today's forecast and Tomorrow's.
(Yeah, I keep older versions to kinda help me see the progress.)
function 68
is where the magic happens.
the new node
allows me to add attributes like MUTE and ALERT too.
The splitter
node is needed to split the message as the messages can get quite long/big.
Google TTS doesn't like messages longer than about 35 words. (Though I think it is more character limited, but anyway.)
And that is send to the enunciater part.
function degToCompass(deg) {
const directions = [
"Northerly", "North North Easterly", "North Easterly", "East North Easterly", "Easterly",
"East South Easterly", "South Easterly", "South South Easterly",
"Southerly", "South South Westerly", "South Westerly", "West South Westerly", "Westerly",
"West North Westerly", "North Westerly", "North North Westerly"
]
const index = Math.round(deg / 22.5) % 16
return directions[index]
}
function getTrend(values, labels = { up: "increasing", down: "decreasing", flat: "stable" }) {
const delta = values[values.length - 1] - values[0]
if (Math.abs(delta) < 0.5) return labels.flat
return delta > 0 ? labels.up : labels.down
}
function formatTemp(val) {
const rounded = Math.round(val * 10) / 10
return Number.isInteger(rounded) ? `${Math.round(rounded)} degrees` : `${rounded.toFixed(1)} degrees`
}
function formatWindSpeed(ms) {
return Math.round(ms * 1.94384) // knots
}
function describeCloudCover(percent) {
if (percent <= 10) return "clear"
if (percent <= 30) return "mostly clear"
if (percent <= 60) return "partly cloudy"
if (percent <= 80) return "mostly cloudy"
return "overcast"
}
function timeFromDateString(dateStr) {
return new Date(dateStr).getHours()
}
function segmentForecasts(forecasts) {
return {
morning: forecasts.filter(f => {
const h = timeFromDateString(f.dt_txt)
return h === 6 || h === 9
}),
daytime: forecasts.filter(f => {
const h = timeFromDateString(f.dt_txt)
return h === 12
}),
afternoon: forecasts.filter(f => {
const h = timeFromDateString(f.dt_txt)
return h === 15
}),
evening: forecasts.filter(f => {
const h = timeFromDateString(f.dt_txt)
return h === 18 || h === 21
})
}
}
function describeConditions(segment) {
const conditionsSet = new Set()
const warnings = []
for (const f of segment) {
const w = f.weather[0].main.toLowerCase()
const desc = f.weather[0].description.toLowerCase()
const rainVol = f.rain?.['3h'] || 0
const snowVol = f.snow?.['3h'] || 0
const vis = f.visibility || 10000
const windGust = f.wind.gust || 0
const temp = f.main.temp
if (temp >= 35 && !warnings.includes("Warning: High temperatures expected")) {
warnings.push("Warning: High temperatures expected")
}
if (temp <= 0 && !warnings.includes("Warning: Freezing temperatures expected")) {
warnings.push("Warning: Freezing temperatures expected")
}
if (w === "rain") {
if (rainVol < 1) conditionsSet.add("light rain")
else if (rainVol < 5) conditionsSet.add("moderate rain")
else {
conditionsSet.add("heavy rain")
if (!warnings.includes("Warning: Heavy rain expected")) {
warnings.push("Warning: Heavy rain expected")
}
}
}
if (w === "snow") {
if (snowVol < 1) {
conditionsSet.add("light snow")
} else {
conditionsSet.add("snow showers")
if (!warnings.includes("Warning: Snowfall expected")) {
warnings.push("Warning: Snowfall expected")
}
}
}
if (w === "thunderstorm") {
conditionsSet.add("thunderstorms")
if (!warnings.includes("Warning: Thunderstorms expected")) {
warnings.push("Warning: Thunderstorms expected")
}
}
if (w === "fog") {
conditionsSet.add("foggy patches")
}
if (vis < 1000 && !warnings.includes("Warning: Very low visibility")) {
warnings.push("Warning: Very low visibility")
}
if (windGust > 20 && !warnings.includes("Warning: Strong wind gusts expected")) {
warnings.push("Warning: Strong wind gusts expected")
}
if (w === "clouds") {
if (!Array.from(conditionsSet).some(c => c.includes("cloud"))) {
conditionsSet.add(desc)
}
} else if (w !== "clear" && w !== "rain" && w !== "snow" && w !== "thunderstorm" && w !== "fog") {
conditionsSet.add(desc)
}
}
const conditionsArray = Array.from(conditionsSet)
const conditionsStr = conditionsArray.length
? conditionsArray.join(" with ")
: "no significant weather"
return {
summary: conditionsStr,
warnings
}
}
function normalizeConditionText(text) {
if (!text) return ""
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()
}
function summarizeSegment(name, data) {
if (data.length === 0) return ""
const temps = data.map(f => f.main.temp)
const clouds = data.map(f => f.clouds.all)
const winds = data.map(f => f.wind.speed)
const windDirs = data.map(f => f.wind.deg)
const trend = getTrend(temps)
const trendLabel = (trend === "stable") ? "no change" : trend
const tempLabel = `${formatTemp(temps[0])} ${trendLabel}`
const cloudLabel = describeCloudCover(clouds[0])
const windSpeed = formatWindSpeed(winds[0])
const windDir = degToCompass(windDirs[0])
const { summary: condSummary, warnings } = describeConditions(data)
const weatherDescription = condSummary !== "clear skies" ? `, ${condSummary}` : ""
const baseSummary = `${name}: Temperature ${tempLabel}, ${cloudLabel} skies${weatherDescription}, Winds ${windDir} at ${windSpeed} knots`
return warnings.length > 0 ? baseSummary + "\n" + warnings.join("\n") : baseSummary
}
function shouldIncludeSegment(name, currentHour, isTodayFlag) {
if (!isTodayFlag) {
return true
}
switch (name) {
case "morning": return currentHour < 12
case "daytime": return currentHour < 16
case "afternoon": return currentHour <= 18
case "evening": return currentHour <= 21
default: return true
}
}
// === MAIN ===
const forecasts = msg.payload
const dayInput = (msg.day || "").toString().trim().toUpperCase()
const now = new Date()
const targetDate = new Date(now)
if (dayInput === "TOMORROW") {
targetDate.setDate(now.getDate() + 1)
} else if (dayInput !== "TODAY") {
msg.payload = `Invalid value for msg.day: expected 'TODAY' or 'TOMORROW'`
return msg
}
const isToday = (dayInput === "TODAY")
const dateStr = targetDate.toLocaleDateString('en-CA')
const forecastsForDay = forecasts.filter(f => {
const localDate = new Date(f.dt_txt).toLocaleDateString('en-CA')
const hour = new Date(f.dt_txt).getHours()
return localDate === dateStr && hour >= 6 && hour <= 21
})
if (forecastsForDay.length < 3) {
msg.payload = `Not enough data for ${dateStr} to generate a forecast.`
return msg
}
const segments = segmentForecasts(forecastsForDay)
const currentHour = now.getHours()
const location = msg.location?.city || "your location"
const dayLabel = (dayInput === "TODAY") ? "Today" : "Tomorrow"
const localTime = now.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
hour12: false
})
const orderedSegments = ["Morning", "Daytime", "Afternoon", "Evening"]
const narrativePieces = []
const allWarnings = new Set()
function createNarrativeSegment(label, data) {
if (!shouldIncludeSegment(label.toLowerCase(), currentHour, isToday)) return null
if (!data || data.length === 0) return null
const temps = data.map(f => f.main?.temp ?? 0)
const clouds = data.map(f => f.clouds?.all ?? 0)
const winds = data.map(f => f.wind?.speed ?? 0)
const windDirs = data.map(f => f.wind?.deg ?? 0)
const trend = getTrend(temps, { up: "warming", down: "cooling", flat: "steady" })
const cloudPhrase = describeCloudCover(clouds[0] || 0)
const conditions = describeConditions(data)
const condSummary = conditions.summary
const warnings = conditions.warnings || []
warnings.forEach(w => allWarnings.add(w))
const windSpeedVal = winds[0] || 0
const windDirVal = windDirs[0] || 0
const windSpeed = formatWindSpeed(windSpeedVal)
const windDir = degToCompass(windDirVal).toLowerCase()
const windPhrase = windSpeed > 5
? `${windSpeed} knot ${windDir} winds`
: null
const conditionPhrase = condSummary && condSummary !== "clear skies"
? normalizeConditionText(condSummary)
: normalizeConditionText(`${cloudPhrase} skies`)
const parts = []
parts.push(`${label}`)
if (conditionPhrase) parts.push(`will be ${conditionPhrase}`)
const tempStr = `${formatTemp(temps[0])}`
if (trend !== "steady") {
parts.push(`${tempStr}, ${trend} temperatures`)
} else {
parts.push(`${tempStr} temperatures`)
}
if (windPhrase) parts.push(windPhrase)
return parts.join(", ")
}
for (let i = 0; i < orderedSegments.length; i++) {
const label = orderedSegments[i]
const data = segments[label.toLowerCase()]
const segmentPhrase = createNarrativeSegment(label, data)
if (segmentPhrase) narrativePieces.push(segmentPhrase)
}
let narrative = ""
if (narrativePieces.length > 0) {
// Capitalize first segment first letter only
narrative = narrativePieces[0].charAt(0).toUpperCase() + narrativePieces[0].slice(1)
for (let i = 1; i < narrativePieces.length; i++) {
const segment = narrativePieces[i].charAt(0).toLowerCase() + narrativePieces[i].slice(1)
narrative += ` and in the ${segment}`
}
// Add final period
narrative += "."
} else {
narrative = "No significant weather forecast available."
}
let warningsText = ""
if (allWarnings.size > 0) {
warningsText = "\n\n⚠️ " + Array.from(allWarnings).join("\n⚠️ ")
}
msg.payload = `${dayLabel}'s forecast for ${location} as of ${localTime}:\n\n${narrative}${warningsText}`
return msg
But I'm wondering is this ........ acceptable? People advocate when making code, to use standard nodes as much as possible.
Which I do get, but for this kind of thing....... Can that really be applicable?
It still isn't perfect and there are some slightly annoying repeats when getting for forecast.
But it is a LONG WAY better than the original.
Just wondering ...... (Well, I'm not exactly sure what I'm wondering)
Thoughts?