Openweather node - am I going over the top with this?

(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? :person_shrugging:

I've implemented something similar...


The Inject nodes (on the left) trigger the Date/Time Formatter at 07:00am and 07:15am each morning. An http-request is made to OpenWeatherMap OneCall facility to get the current day's forecast. The result is parsed to make a personalised report like...
"Hello Dave and Chris here is your weather forecast for xxxx. Today will be..... etc, etc. Tomorrow will be... etc, etc. Time now for a cup of tea. Goodbye and have a nice day."
The message is sent to an Alexa in a designated room (as specified in the Inject node).

This is what is in my 'Parse Forecast' node...

let forecast = "";
// current weather
forecast = forecast +msg.motd+msg.name+"<break time='0.25s'/> please standby <break time='0.5s'/> here is your weather report for Yateley";
forecast = forecast + "<break time='0.25s'/> The weather is currently " + msg.payload.current.weather[0].description + " with a temperature of " + Math.floor(msg.payload.current.temp) + " degrees.";

// forecast for today
forecast = forecast + "<break time='0.25s'/> today you can " + msg.payload.daily[0].summary.toLowerCase();

// Convert wind bearing to direction
var WindDirection = "";
if (msg.payload.daily[0].wind_deg>=0 && msg.payload.daily[0].wind_deg<22.5) {
    WindDirection = "North";
}
if (msg.payload.daily[0].wind_deg>=22.5 && msg.payload.daily[0].wind_deg<67.5) {
    WindDirection = "North-East";
}
if (msg.payload.daily[0].wind_deg>=67.5 && msg.payload.daily[0].wind_deg<112.5) {
    WindDirection = "East";
}
if (msg.payload.daily[0].wind_deg>=112.5 && msg.payload.daily[0].wind_deg<157.5) {
    WindDirection = "South-East";
}
if (msg.payload.daily[0].wind_deg>=157.5 && msg.payload.daily[0].wind_deg<202.5) {
    WindDirection = "South";
}
if (msg.payload.daily[0].wind_deg>=202.5 && msg.payload.daily[0].wind_deg<247.5) {
    WindDirection = "South-West";
}
if (msg.payload.daily[0].wind_deg>=247.5 && msg.payload.daily[0].wind_deg<292.5) {
    WindDirection = "West";
}
if (msg.payload.daily[0].wind_deg>=292.5 && msg.payload.daily[0].wind_deg<337.5) {
    WindDirection = "North-West";
}
if (msg.payload.daily[0].wind_deg>=337.5) {
    WindDirection = "North";
}
forecast = forecast + " <break time='0.25s'/> Wind speed is " + Math.floor(msg.payload.daily[0].wind_speed) + " kilometers per hour from " + WindDirection + ".";

// Calculate sunset time
var sunset = new Date(msg.payload.daily[0].sunset*1000);

forecast = forecast + " Sunset is at " + sunset.getHours() + ":" + sunset.getMinutes() + ".";

forecast = forecast + "<break time='0.25s'/> Tomorrow you can " + msg.payload.daily[1].summary.toLowerCase();

forecast = forecast + "<break time='0.5s'/> Time for a pot of tea now I think <break time='0.25s'/> Good bye and have a nice day";


msg.payload = forecast;
return msg;

I'm lazy, I just break my day into three parts inside the MOTD Greeting node...

if (msg.motd <13) {
    msg.motd="Good morning ";
}
else if ((msg.motd >12) && (msg.motd <19)){
    msg.motd = "Good afternoon ";
}
else msg.motd = "Good evening ";
return msg;

I've been using this NR flow for 4+ years without any issues.
Note: If we are sleeping in the 'front' bedroom we usually listen to the news at 7:00am, so that's why I schedule the weather forecast for 7:15am in that room so it doesn't clash.

Yeah, I have a couple of versions....
The first one breaks the day into 4 parts.
Morning, day, afternoon and evening.

It then reads out in a kind of acceptable way the 4 summaries.

But I wanted to see if I could get it better and have this way.

Alas it is fairly new and I haven't tried it many times.

I'm wanting a nicer output, but that is still to happen.

But all that JS code in the function node......
That's not breaking any of the rules?

Ok, that is me being a bit harsh on myself. But I am still not sure if it is ..... viable to learn this or what alternate options there are.

This is, of course, an ongoing debate in the community. But what are rules for if not for occasionally breaking! :slight_smile:

Seriously though, Node-RED is a TOOL - you aren't at school, you are free to do whatever works for you.

If you are happy with code, use code. If it irk's you, use nodes. Your choice.

For complex flows, I used to often create the logic using nodes and then, I might later decide that was too messy and collapse some of it down to code in a function node. These days, I'm so used to the quirks of JavaScript that I'll often go straight into a function node rather than being forced to do all that tiresome dragging and dropping of nodes and wires. :rofl:

The only thing I would say about longer function nodes is - add decent comments so that you can understand the code in 2 years time when you have to re-visit it.

So I regularly now use Node-RED as a platform for event-driven processes where the core logic is in code and the event handling and output (either via Telegram or UIBUILDER) is handled by nodes. That's the nice thing about Node-RED, you are not forced down a particular style.

One other possible benefit of keeping logic in code is that, should you need to, you can more easily transfer it to a separate process. If, for example, you needed a performance boost where Node-RED was simply getting in the way or where you wanted to move the logic to its own thread.

Thanks.

And also thanks for helping me better understand my concern.

Yes, we aren't at school. and I can do what I want.

But I feel it is better for me in the long run to only develop good habits and not bad ones.

That may be the core of my confusion/concern.

offtopic, to save some code:

const wind_deg = 22 //msg.payload.daily[0].wind_deg
const degrees = ((wind_deg % 360) + 360) % 360
const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
const index = Math.round((degrees + 11.25) / 22.5) % 16 // depending on the number of directions, 16 in this case
const wind_direction = directions[index] // result: NNE

@Trying_to_learn

But I feel it is better for me in the long run to only develop good habits and not bad ones.

In the end it is all about the end result that the function node should output, how to get there can be efficient / fast, or not, but the output could still be what you need. It is more about bad/good practices than habits i believe.

3 Likes

Thanks - that's neat and compact.
I've taken your suggestion - modified it for 8-points-of-the-compass and included directions with full names. Works just fine. Thanks again.

let forecast = "";

// current weather
forecast = forecast +msg.motd+msg.name+"<break time='0.25s'/> please standby <break time='0.5s'/> here is your weather report for Yateley";
forecast = forecast + "<break time='0.25s'/> The weather is currently " + msg.payload.current.weather[0].description + " with a temperature of " + Math.floor(msg.payload.current.temp) + " degrees.";

// forecast for today
forecast = forecast + "<break time='0.25s'/> today you can " + msg.payload.daily[0].summary.toLowerCase();

// convert wind bearing to direction
const wind_deg = msg.payload.daily[0].wind_deg
const degrees = ((wind_deg % 360) + 360) % 360

// 8 directions with full names
const directions = [
    "North",
    "North-East",
    "East",
    "South-East",
    "South",
    "South-West",
    "West",
    "North-West"
]

// 45° sectors → add half-sector (22.5°) to round correctly
const index = Math.round(degrees / 45) % 8
const wind_direction = directions[index]

forecast = forecast + " <break time='0.25s'/> Wind speed is " + Math.floor(msg.payload.daily[0].wind_speed) + " kilometers per hour from " + wind_direction + ".";

// calculate sunset time
var sunset = new Date(msg.payload.daily[0].sunset*1000);

forecast = forecast + " Sunset is at " + sunset.getHours() + ":" + sunset.getMinutes() + ".";
forecast = forecast + "<break time='0.25s'/> Tomorrow you can " + msg.payload.daily[1].summary.toLowerCase();
forecast = forecast + "<break time='0.5s'/> Time for a pot of tea now I think <break time='0.25s'/> Good bye and have a nice day";

msg.payload = forecast;
return msg;

By-the-way... although the JS in the function node might be lengthly, I don't mind - as I know if I come back to it in a few months time I will still be able to understand it.

Thanks, apreciated.

I haven't thought of it in that way before.

I'm on a roll now... Taken a look at the JS I wrote 4++ years ago and decided to clean it up by using 'template literals' and implementing @bakman2 suggestion. Here's the result...

// modified to use template literals 
// and sector-based mapping for compass points 

let short_break = '0.25s';
let long_break  = '0.5s';

let forecast = "";

// current weather
forecast += `${msg.motd}${msg.name}<break time='${short_break}'/> please standby <break time='${long_break}'/> here is your weather report for Yateley.`;
forecast += `<break time='${short_break}'/> The weather is currently ${msg.payload.current.weather[0].description} with a temperature of ${Math.floor(msg.payload.current.temp)} degrees.`;

// forecast for today
forecast += `<break time='${short_break}'/> Today you can ${msg.payload.daily[0].summary.toLowerCase()}`;

// convert wind bearing to direction
const wind_deg = msg.payload.daily[0].wind_deg;
const degrees = ((wind_deg % 360) + 360) % 360;

// 8 directions with full names
const directions = [
    "North",
    "North-East",
    "East",
    "South-East",
    "South",
    "South-West",
    "West",
    "North-West"
];

const index = Math.round(degrees / 45) % 8;
const wind_direction = directions[index];

// wind report
forecast += ` <break time='${short_break}'/> Wind speed is ${Math.floor(msg.payload.daily[0].wind_speed)} kilometers per hour from ${wind_direction}.`;

// calculate sunset time
let sunset = new Date(msg.payload.daily[0].sunset * 1000);
let sunsetHours = sunset.getHours().toString().padStart(2, '0');
let sunsetMinutes = sunset.getMinutes().toString().padStart(2, '0');

forecast += ` Sunset is at ${sunsetHours}:${sunsetMinutes}.`;
forecast += `<break time='${short_break}'/> Tomorrow you can ${msg.payload.daily[1].summary.toLowerCase()}.`;
forecast += `<break time='${long_break}'/> Time for a pot of tea now I think <break time='${short_break}'/> Good bye and have a nice day.`;

// return forecast
msg.payload = forecast;
return msg;

What I am saying is that your concerns are not really well founded. It is right to have some thought over how you are doing your flows. But ultimately, what matters is, can the flows be understood later? If they can, then Node-RED has few hard and fast rules. A chunk of logic in a function node is every bit as valid as a long string of nodes.

Whether you are using nodes or function code, make sure things are well laid out, well commented. Where using code, try to follow good practice (though give yourself a bit of slack - we are all learning and not everything has to be perfect).

3 Likes

I don't think those change? If so, they should be const not let :smiley:

With the template literals, don't forget you can use multiple lines which makes for really nice, easy to read, using += for strings is relatively slow compared to using a straight multi-line template literal.

1 Like

In fact I can change all the let statements to const except for let forecast = ""

// last edited 09-Sept-2025
// modified to use template literals 
// and sector-based mapping for compass points 

const short_break = '0.25s';
const long_break  = '0.5s';

let forecast = "";

// current weather
forecast += `${msg.motd}${msg.name}<break time='${short_break}'/> please standby <break time='${long_break}'/> here is your weather report for Yateley.`;
forecast += `<break time='${short_break}'/> The weather is currently ${msg.payload.current.weather[0].description} with a temperature of ${Math.floor(msg.payload.current.temp)} degrees.`;

// forecast for today
forecast += `<break time='${short_break}'/> Today you can ${msg.payload.daily[0].summary.toLowerCase()}`;

// convert wind bearing to direction
const wind_deg = msg.payload.daily[0].wind_deg;
const degrees = ((wind_deg % 360) + 360) % 360;

// 8 directions with full names
const directions = [
    "North",
    "North-East",
    "East",
    "South-East",
    "South",
    "South-West",
    "West",
    "North-West"
];

const index = Math.round(degrees / 45) % 8;
const wind_direction = directions[index];

// wind report
forecast += ` <break time='${short_break}'/> Wind speed is ${Math.floor(msg.payload.daily[0].wind_speed)} kilometers per hour from ${wind_direction}.`;

// calculate sunset time
const sunset = new Date(msg.payload.daily[0].sunset * 1000);
const sunsetHours = sunset.getHours().toString().padStart(2, '0');
const sunsetMinutes = sunset.getMinutes().toString().padStart(2, '0');

forecast += ` Sunset is at ${sunsetHours}:${sunsetMinutes}.`;
forecast += `<break time='${short_break}'/> Tomorrow you can ${msg.payload.daily[1].summary.toLowerCase()}.`;
forecast += `<break time='${long_break}'/> Time for a pot of tea now I think <break time='${short_break}'/> Good bye and have a nice day.`;

// return forecast
msg.payload = forecast;
return msg;


// current weather
let forecast = `
  ${msg.motd}${msg.name}
  <break time='${short_break}'/>
  please standby
  <break time='${long_break}'/>
  here is your weather report for Yateley.

  <break time='${short_break}'/> The weather is currently
  ${msg.payload.current.weather[0].description} with a temperature of
  ${Math.floor(msg.payload.current.temp)} degrees.
`

Is that not easier to read?

2 Likes

I much prefer code for complex tasks than simple nodes. In fact I prefer function nodes most of the time, because it’s easier for me to quickly understand code than yet another gui tool with arbitrary interface. But it’s all a balance, I can enjoy nodes that do things I’m not so happy about coding, for example http requests. So it all depends really. And even when using function nodes a lot, it can still be split up and divided when needed to avoid too much code in one place. Some people use code for everything, some don’t use code at all. No right answer here, just whatever is easiest to understand and maintain.

3 Likes

I don't find that easier to read:

My inner debug engine screams out "unquoted string" when it meets a line like

please standby

I wonder about the significance of carriage returns and blank lines.

A lone backtick on a line is very easy to miss when scan reading the code.
I also much find it much too easy to omit the matching ` at the end (as you do too apparently :smiley:)

The

`'${short_break}'`

syntax looks like it might be useful though, I'll have to work out the significance of the various quotes.

For HTML, there is no difference between them and a space. (except for the <pre> tags and the equivalent CSS).

It is better with a better font. And the linting highlights errors anyway.

Nope, it is there but the font that Discourse uses isn't very clear.

You can safely nest the 3 different string quote types. Double and single quotes are plain strings. I prefer to use single for code. Partly because it doesn't need a shift-key press and partly because HTML generally prefers double (though actually, you should be able to use both I think). Back-tick quotes are different as they are, as mentioned, template literals. Multi-line capable, handle code inside ${...}. You can also create custom template literals but that is a different subject.

Template literals are generally more performant than repeated "xxx" + "yyyy" type string catenations.

Aside: I REALLY do wish that inline code in Discourse had a different background colour, the change made a while back is definitely problematic for recognising code snippets.

1 Like

I should have used the copy button instead of drag, ctrl-c!

I do sometimes use single quotes around a block of code, for example wrapped around a whole shell script to pass to the exec node.
It seems clear that I can't be trusted with these things though :nerd_face:

1 Like

(just to share - and reading a couple of recent posts)

My code gets the forecast and creates a message to be passed to the google TTS node.

Now and then an empty line appears in what is made.

To clean things up as much as possible before hand I strip out any illegal characters to stop the TTS node throwing a wobbly.
This (sometimes) included empty lines.